Skip to main content
Glama

Dynatrace MCP Server

Official
dynatrace-clients.ts8.95 kB
import { HttpClient, PlatformHttpClient } from '@dynatrace-sdk/http-client'; import { getSSOUrl } from 'dt-app'; import { getUserAgent } from '../utils/user-agent'; import { performOAuthAuthorizationCodeFlow, refreshAccessToken } from './dynatrace-oauth-auth-code-flow'; import { globalTokenCache } from './token-cache'; import { getRandomPort } from './utils'; import { requestTokenForClientCredentials } from './dynatrace-oauth-client-credentials'; /** * Create a Dynatrace Http Client (from the http-client SDK) based on the provided authentication credentials * Supports Platform Token, OAuth Client Credentials Flow, and OAuth Authorization Code Flow (interactive) * @param environmentUrl * @param scopes * @param clientId * @param clientSecret * @param dtPlatformToken * @returns an authenticated HttpClient */ export const createDtHttpClient = async ( environmentUrl: string, scopes: string[], clientId?: string, clientSecret?: string, dtPlatformToken?: string, ): Promise<HttpClient> => { /** Logic: * * if a platform token is provided, use it * * If no platform token is provided, but clientId and clientSecret are provided, use client credentials flow * * If no platform token is provided, and no clientSecret is provided, but a clientId is provided, use OAuth authorization code flow (interactive) * * If neither platform token nor OAuth credentials are provided, throw an error */ if (dtPlatformToken) { // create a simple HTTP client if only the platform token is provided return createPlatformTokenHttpClient(environmentUrl, dtPlatformToken); } else if (clientId && clientSecret) { // create an Oauth client using client credentials flow (non-interactive) return createOAuthClientCredentialsHttpClient(environmentUrl, scopes, clientId, clientSecret); } else if (clientId) { // create an OAuth client using authorization code flow (interactive) return createOAuthAuthCodeFlowHttpClient(environmentUrl, scopes, clientId); } throw new Error( 'Failed to create Dynatrace HTTP Client: Please provide either clientId and clientSecret for client credentials flow, clientId only for interactive OAuth flow, or just a platform token.', ); }; /** * Creates an HTTP Client based on environmentUrl and a bearer token, and also sets the user agent */ const createBearerTokenHttpClient = async (environmentUrl: string, bearerToken: string): Promise<HttpClient> => { return new PlatformHttpClient({ baseUrl: environmentUrl, defaultHeaders: { 'Authorization': `Bearer ${bearerToken}`, 'User-Agent': getUserAgent(), }, }); }; /** * Creates an HTTP Client based on environmentUrl and a platform token (as bearer token) */ const createPlatformTokenHttpClient = async (environmentUrl: string, dtPlatformToken: string): Promise<HttpClient> => { console.error(`🔒 Using Platform Token to authenticate API Calls to ${environmentUrl}`); return createBearerTokenHttpClient(environmentUrl, dtPlatformToken); }; /** * Create an OAuth Client based on clientId, clientSecret, environmentUrl and scopes * This uses a client-credentials flow to request a token from the SSO endpoint. * Note: We do not refresh the token here, we always request a new one on each client creation. */ const createOAuthClientCredentialsHttpClient = async ( environmentUrl: string, scopes: string[], clientId: string, clientSecret: string, ): Promise<HttpClient> => { console.error( `🔒 Client-Creds-Flow: Trying to authenticate API Calls to ${environmentUrl} via OAuthClientId ${clientId} with the following scopes: ${scopes.join(', ')}`, ); // Get SSO Base URL const ssoBaseURL = await getSSOUrl(environmentUrl); // try to request a token, just to verify that everything is set up correctly const tokenResponse = await requestTokenForClientCredentials(clientId, clientSecret, ssoBaseURL, scopes); // in case we didn't get a token, or error / error_description / issueId is set, we throw an error if (!tokenResponse.access_token || tokenResponse.error || tokenResponse.error_description || tokenResponse.issueId) { throw new Error( `Failed to retrieve OAuth token (IssueId: ${tokenResponse.issueId}): ${tokenResponse.error} - ${tokenResponse.error_description}. Note: Your OAuth client is most likely not configured correctly and/or is missing scopes.`, ); } console.error( `Successfully retrieved token from SSO! Token valid for ${tokenResponse.expires_in}s with scopes: ${tokenResponse.scope}`, ); // now that we have the access token, we can just use a plain bearer token client return createBearerTokenHttpClient(environmentUrl, tokenResponse.access_token); }; /** Create an OAuth Client using authorization code flow (interactive authentication) * This starts a local HTTP server to handle the OAuth redirect and requires user interaction. * Implements an in-memory token cache (not persisted to disk). After every server restart a new * authentication flow (or token refresh) may be required. * Note: Always requests a complete set of scopes for maximum token reusability. Else the user will end up having to approve multiple requests. */ const createOAuthAuthCodeFlowHttpClient = async ( environmentUrl: string, scopes: string[], clientId: string, ): Promise<HttpClient> => { // Get SSO Base URL const ssoBaseURL = await getSSOUrl(environmentUrl); // Fast Track: Fetch cached token and check if it is still valid const cachedToken = globalTokenCache.getToken(scopes); const isValid = globalTokenCache.isTokenValid(scopes); // If we have a valid cached token, we can use it if (isValid && cachedToken) { const expiresIn = cachedToken.expires_at ? Math.round((cachedToken.expires_at - Date.now()) / 1000) : 'never'; console.error(`✅ Auth-Code-Flow: Using cached access token (expires in ${expiresIn}s)`); // just use the cached token as a bearer token return createBearerTokenHttpClient(environmentUrl, cachedToken.access_token); } // If we have an expired token that can be refreshed, refresh it if (cachedToken && cachedToken.refresh_token && !isValid) { const expiresIn = cachedToken.expires_at ? Math.round((cachedToken.expires_at - Date.now()) / 1000) : 'never'; console.error(`🔍 Auth-Code-Flow: Found expired cached token (expires in ${expiresIn}s), attempting refresh...`); try { console.error(`🔄 Attempting to refresh expired access token...`); const tokenResponse = await refreshAccessToken(ssoBaseURL, clientId, cachedToken.refresh_token, scopes); if (tokenResponse.access_token && !tokenResponse.error) { console.error(`✅ Successfully refreshed access token!`); // Update the cache with the new token globalTokenCache.setToken(scopes, tokenResponse); // now use the updated token as a bearer token return createBearerTokenHttpClient(environmentUrl, tokenResponse.access_token); } else { console.error(`❌ Token refresh failed: ${tokenResponse.error} - ${tokenResponse.error_description}`); // Clear the invalid token from cache globalTokenCache.clearToken(); } } catch (error) { console.error(`❌ Token refresh failed with error: ${error instanceof Error ? error.message : String(error)}`); // Clear the invalid token from cache globalTokenCache.clearToken(); } } // If we get here, we are currently not authenticated, and need to perform a full OAuth Authorization Code Flow console.error(`🚀 Auth-Code-Flow: No valid cached token found, initiating OAuth Authorization Code Flow...`); console.error(`Using SSO base URL ${ssoBaseURL}`); // Randomly select a port for the OAuth redirect URL (e.g., 5344) const port = getRandomPort(); // Perform the OAuth authorization code flow with all scopes const tokenResponse = await performOAuthAuthorizationCodeFlow( ssoBaseURL, { clientId, // redirectUri will be used as a redirect/callback from the authorization code flow redirectUri: `http://localhost:${port}/auth/login`, scopes: scopes, // Request all scopes upfront }, port, ); // Check if we got a valid token if (!tokenResponse.access_token || tokenResponse.error || tokenResponse.error_description || tokenResponse.issueId) { throw new Error( `Failed to retrieve OAuth token via authorization code flow (IssueId: ${tokenResponse.issueId}): ${tokenResponse.error} - ${tokenResponse.error_description}`, ); } // Cache the new token with all scopes globalTokenCache.setToken(scopes, tokenResponse); console.error( `✅ Successfully retrieved token from SSO! Token cached for future use with scopes: ${scopes.join(', ')}`, ); // now that we have the access token, we can just use a plain bearer token client return createBearerTokenHttpClient(environmentUrl, tokenResponse.access_token); };

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/dynatrace-oss/dynatrace-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server