Guide · Enterprise Observability
MCP Tools for Dynatrace — metrics API v2, problems, and entity queries
Dynatrace is an AI-driven observability platform built around an entity model: every monitored component (host, service, process, synthetic test, cloud resource) is represented as an entity with a stable entityId. When building MCP tools that interact with Dynatrace — querying service response time metrics, listing active problems, inspecting entity topology, or reading alerting profiles — three distinctions are critical: environment API vs platform API (the environment API at https://<env-id>.live.dynatrace.com/api/v2/ is the primary programmatic interface; the Platform API requires an OAuth2 token and is used for tenant management), Dynatrace Metrics API v2 vs v1 (v2 supports metric selectors, resolution, and transformation — v1 timeseries API is deprecated), and problems vs alerts vs events (Davis AI-detected problems are the authoritative incident signal; alerts are rule-based threshold triggers that may or may not create a problem; events are raw observations attached to entities).
TL;DR
Authenticate with Authorization: Api-Token <dt0c01.xxx>. The environment API base URL is https://<env-id>.live.dynatrace.com/api/v2/. Query metrics with GET /metrics/query?metricSelector=<selector>&resolution=1m&from=now-1h. List active problems with GET /problems?problemStatus=OPEN. Get entities with GET /entities?entitySelector=type(SERVICE)&fields=properties,toRelationships. All API tokens require specific scopes — query the metric API with the metrics.read scope, problems with problems.read, and entities with entities.read. Token scopes are specified at creation and cannot be modified — if a scope is missing, the API returns HTTP 403 with a MISSING_PERMISSION error.
Dynatrace API authentication and token scopes
Dynatrace uses API tokens with scope-based access control. Each token is created in the Dynatrace UI (Settings → Integration → Dynatrace API) or via the Settings API with a specific set of scopes. The token format is dt0c01.<publicPart>.<secretPart> — the public part is visible in the UI and identifies the token; the secret part is shown only once at creation and cannot be retrieved later. If the secret part is lost, the token must be revoked and recreated.
MCP tools should use tokens with only the required scopes. A monitoring-only MCP tool needs metrics.read + problems.read + entities.read. A tool that creates maintenance windows needs settings.write. A tool that invokes synthetic tests needs syntheticExecutions.write. Never use a token with DataExport or settings.write for read-only tools — the blast radius of token compromise is proportional to the granted scopes.
async function createDynatraceClient(config) {
const {
environmentId, // e.g. 'abc12345'
apiToken, // dt0c01.xxx.yyy
} = config;
const baseUrl = `https://${environmentId}.live.dynatrace.com/api/v2`;
const headers = {
'Authorization': `Api-Token ${apiToken}`,
'Content-Type': 'application/json',
'Accept': 'application/json',
};
async function get(path, params = {}) {
const url = new URL(`${baseUrl}${path}`);
for (const [k, v] of Object.entries(params)) {
if (v !== undefined && v !== null) url.searchParams.set(k, String(v));
}
const response = await fetch(url.toString(), { headers });
if (response.status === 403) {
const body = await response.json().catch(() => ({}));
const missingScope = body.details?.missingPermissions?.[0];
throw new Error(
`Dynatrace 403 Forbidden — token missing scope: ${missingScope ?? 'unknown'}. ` +
`Add '${missingScope}' to the API token in Settings → Integration → Dynatrace API.`
);
}
if (!response.ok) {
const body = await response.json().catch(() => ({ error: { message: response.statusText } }));
throw new Error(`Dynatrace ${path} → ${response.status}: ${body.error?.message}`);
}
return response.json();
}
async function post(path, body) {
const response = await fetch(`${baseUrl}${path}`, {
method: 'POST',
headers,
body: JSON.stringify(body),
});
if (!response.ok) {
const errBody = await response.json().catch(() => ({}));
throw new Error(`Dynatrace POST ${path} → ${response.status}: ${errBody.error?.message}`);
}
return response.status === 204 ? null : response.json();
}
return { get, post, baseUrl };
}
const dt = await createDynatraceClient({
environmentId: 'abc12345',
apiToken: 'dt0c01.EXAMPLE.SECRETPART',
});
Metrics API v2: selectors, resolution, and transformation
The Dynatrace Metrics API v2 uses a metricSelector string that identifies which metric to query and optionally specifies aggregations, filters, and transformations inline. The selector format is metricKey:aggregation:transformation:filter. For example, builtin:service.response.time:percentile(95):names:filter(eq(dt.entity.service,SERVICE-12345)) returns the 95th percentile response time for a specific service with dimension names (human-readable entity names) instead of raw entity IDs.
A key operational gotcha: Dynatrace metrics may have data gaps if OneAgent loses connectivity or if the monitored process restarts. The resolution parameter controls the time bucket size; if a bucket has no data, the response includes a null value for that timestamp. MCP tools must handle null values in metric data points — treating null as zero will produce incorrect dashboards and false "good health" readings during a genuine outage. Check for nulls explicitly and report them as "data unavailable" rather than converting them to zero.
async function queryMetrics(dt, params) {
const {
metricSelector, // e.g. 'builtin:service.response.time:percentile(95)'
from = 'now-1h', // e.g. 'now-1h', 'now-30m', or ISO timestamp
to = 'now',
resolution = '1m', // Data point granularity: '1m', '5m', '1h', 'Inf' for rollup
entitySelector, // Optional: filter to specific entities
mzSelector, // Optional: filter to a Management Zone
} = params;
const queryParams = {
metricSelector,
from,
to,
resolution,
...(entitySelector && { entitySelector }),
...(mzSelector && { mzSelector }),
};
const result = await dt.get('/metrics/query', queryParams);
// result.resolution: actual resolution used (may differ from requested)
// result.result: array of MetricSeriesCollection objects
return result.result.map((series) => ({
metricId: series.metricId,
// Each data contains: dimensionMap (entity->value mappings), timestamps, values
data: series.data.map((d) => ({
dimensions: d.dimensionMap, // e.g. { 'dt.entity.service': 'SERVICE-123', 'service.name': 'checkout' }
timestamps: d.timestamps, // Array of Unix ms timestamps
values: d.values, // Array of numbers or nulls — null means no data in that bucket
nullCount: d.values.filter((v) => v === null).length,
hasGaps: d.values.some((v) => v === null),
})),
resolution: result.resolution,
warningMessages: result.warnings,
}));
}
// Get metric descriptor: units, aggregations, and description
async function describeMetric(dt, metricKey) {
const result = await dt.get(`/metrics/${encodeURIComponent(metricKey)}`);
return {
metricId: result.metricId,
displayName: result.displayName,
description: result.description,
unit: result.unit,
// Available aggregations for this metric
aggregationTypes: result.aggregationTypes, // e.g. ['auto', 'avg', 'max', 'min', 'percentile']
// Dimensions this metric can be filtered/split by
dimensionDefinitions: result.dimensionDefinitions,
entityType: result.entityType, // e.g. 'SERVICE', 'HOST', 'PROCESS_GROUP_INSTANCE'
};
}
// List available metrics matching a pattern
async function searchMetrics(dt, prefix) {
const result = await dt.get('/metrics', {
metricSelector: `${prefix}*`,
fields: 'displayName,unit,aggregationTypes',
pageSize: 50,
});
return result.metrics ?? [];
}
Problems API: active incidents and Davis AI root cause analysis
Dynatrace's Davis AI automatically detects anomalies, correlates related symptoms, and opens "problems" — each problem represents a set of related infrastructure and application issues with an identified root cause. The Problems API v2 allows MCP tools to list active problems, retrieve problem details including the root cause entity and impact analysis, and add comments or close problems programmatically.
Problems have three statuses: OPEN (currently active), RESOLVED (auto-resolved by Davis AI when metrics return to baseline), and CLOSED (manually closed via API or UI). The affectedEntities array lists all entities impacted by the problem, while rootCauseEntity identifies the entity Davis AI determined to be the causal component. MCP tools should surface the root cause entity and its type to operators — a SERVICE root cause requires different remediation than an INFRASTRUCTURE root cause.
async function listProblems(dt, options = {}) {
const {
status = 'OPEN', // 'OPEN' | 'RESOLVED' | 'CLOSED'
from = 'now-2h',
to = 'now',
entitySelector, // Optional: filter to a specific entity's problems
impactLevel, // 'SERVICE' | 'APPLICATION' | 'INFRASTRUCTURE' | 'ENVIRONMENT'
severityLevel, // 'AVAILABILITY' | 'ERROR' | 'PERFORMANCE' | 'RESOURCE_CONTENTION' | 'CUSTOM_ALERT'
} = options;
const result = await dt.get('/problems', {
problemStatus: status,
from,
to,
...(entitySelector && { entitySelector }),
...(impactLevel && { impactLevel }),
...(severityLevel && { severityLevel }),
pageSize: 50,
fields: '+affectedEntities,+recentComments,+rootCauseEntity,+evidenceDetails',
});
return result.problems.map((p) => ({
problemId: p.problemId,
displayId: p.displayId, // Human-readable ID like 'P-12345'
title: p.title,
status: p.status,
impactLevel: p.impactLevel, // Who is affected: SERVICE/APPLICATION/INFRASTRUCTURE/ENVIRONMENT
severityLevel: p.severityLevel, // Type of issue: AVAILABILITY/ERROR/PERFORMANCE/etc.
startTime: new Date(p.startTime).toISOString(),
endTime: p.endTime ? new Date(p.endTime).toISOString() : null,
durationMs: p.endTime ? p.endTime - p.startTime : Date.now() - p.startTime,
rootCause: p.rootCauseEntity ? {
entityId: p.rootCauseEntity.entityId,
name: p.rootCauseEntity.name,
type: p.rootCauseEntity.type,
} : null,
affectedEntities: (p.affectedEntities ?? []).map((e) => ({
entityId: e.entityId,
name: e.name,
type: e.type,
})),
affectedCount: p.affectedEntities?.length ?? 0,
recentComments: p.recentComments?.comments?.slice(0, 3) ?? [],
// hasKnownRootCause: if false, Davis AI is still investigating
hasKnownRootCause: !!p.rootCauseEntity,
}));
}
// Get full problem details including evidence
async function getProblemDetails(dt, problemId) {
const problem = await dt.get(`/problems/${problemId}`, {
fields: '+affectedEntities,+recentComments,+rootCauseEntity,+evidenceDetails,+impactAnalysis',
});
return {
...problem,
// Impact analysis shows service degradation percentages
impactAnalysis: problem.impactAnalysis,
// Evidence details list the specific anomalies Davis AI detected
evidenceDetails: problem.evidenceDetails?.details ?? [],
};
}
// Add a comment to a problem (for acknowledgement or runbook links)
async function commentOnProblem(dt, problemId, message, authorName) {
await dt.post(`/problems/${problemId}/comments`, {
message,
context: authorName, // Shown as the comment author in the Dynatrace UI
});
}
Entity API: service topology and dependency mapping
The Dynatrace entity model tracks monitored components as entities with stable IDs and typed relationships. Services call other services, processes run on hosts, hosts belong to host groups, and Kubernetes pods run within namespaces. MCP tools can traverse this topology to answer questions like "which services depend on this database?" or "what are all the components in this Kubernetes namespace?"
Entity queries use an entitySelector — a filter expression that specifies entity type, tags, management zones, and relationship traversal. The from and to parameters scope the query to entities that were observed within a time window (useful for excluding decommissioned entities that still appear in the entity store). The fields parameter controls which additional data is included — by default only the entity ID, name, and type are returned; specify +properties,+toRelationships,+fromRelationships,+tags for the full picture.
async function queryEntities(dt, params) {
const {
entitySelector, // e.g. 'type(SERVICE)', 'type(HOST),tag(env:prod)', 'type(PROCESS_GROUP_INSTANCE)'
from = 'now-3h',
to = 'now',
fields = '+properties,+tags,+toRelationships,+fromRelationships',
pageSize = 50,
} = params;
const allEntities = [];
let nextPageKey = null;
do {
const result = await dt.get('/entities', {
entitySelector,
from,
to,
fields,
pageSize,
...(nextPageKey && { nextPageKey }),
});
allEntities.push(...(result.entities ?? []));
nextPageKey = result.nextPageKey ?? null;
} while (nextPageKey);
return allEntities.map((e) => ({
entityId: e.entityId,
displayName: e.displayName,
type: e.type,
firstSeen: e.firstSeenTms ? new Date(e.firstSeenTms).toISOString() : null,
lastSeen: e.lastSeenTms ? new Date(e.lastSeenTms).toISOString() : null,
tags: (e.tags ?? []).map((t) => `${t.context}:${t.key}${t.value ? `=${t.value}` : ''}`),
properties: e.properties ?? {},
// Relationships: what this entity calls (toRelationships) and what calls it (fromRelationships)
calls: (e.toRelationships?.calls ?? []).map((r) => ({ entityId: r.id, type: r.type })),
calledBy: (e.fromRelationships?.calledBy ?? []).map((r) => ({ entityId: r.id, type: r.type })),
runsOn: (e.toRelationships?.runsOn ?? []).map((r) => r.id),
}));
}
// Get entities impacted by a specific entity's problems
async function getServiceDependencies(dt, serviceEntityId) {
// Find everything this service calls
const [service] = await queryEntities(dt, {
entitySelector: `entityId(${serviceEntityId})`,
fields: '+toRelationships,+fromRelationships',
});
if (!service) throw new Error(`Service entity not found: ${serviceEntityId}`);
return {
service: { entityId: service.entityId, name: service.displayName },
calls: service.calls, // Services this service calls (downstream dependencies)
calledBy: service.calledBy, // Services that call this service (upstream consumers)
};
}
AliveMCP integration for Dynatrace environments
Dynatrace itself is a SaaS or managed service, so the Dynatrace environment is not something you probe for uptime in the traditional sense — Dynatrace's own SLA is maintained by Dynatrace. Instead, AliveMCP monitors your monitored systems through Dynatrace by polling the Problems API for open incidents and the Metrics API for SLO thresholds.
A practical integration: register an AliveMCP custom probe that calls the Dynatrace Problems API and treats any OPEN problem with impactLevel=SERVICE and severityLevel=AVAILABILITY as a DOWN event. This turns AliveMCP into a second layer of alerting that fires independently of Dynatrace's built-in alerting profiles, providing defense in depth against missed notifications.
// AliveMCP health probe: synthesize MCP server health from Dynatrace data
async function mcpServerHealthViaDynatrace(dt, mcpServiceEntityId) {
const [problems, metrics] = await Promise.allSettled([
// Check for open availability problems on this service
listProblems(dt, {
status: 'OPEN',
entitySelector: `entityId(${mcpServiceEntityId})`,
severityLevel: 'AVAILABILITY',
}),
// Query error rate metric
queryMetrics(dt, {
metricSelector: `builtin:service.errors.total.rate`,
from: 'now-5m',
resolution: 'Inf', // Single aggregated value
entitySelector: `entityId(${mcpServiceEntityId})`,
}),
]);
const openProblems = problems.status === 'fulfilled' ? problems.value : [];
const errorRate = metrics.status === 'fulfilled'
? metrics.value?.[0]?.data?.[0]?.values?.[0] ?? null
: null;
return {
// HTTP 200 = UP for AliveMCP; non-200 = DOWN
healthy: openProblems.length === 0,
openAvailabilityProblems: openProblems.length,
errorRatePercent: errorRate !== null ? Math.round(errorRate * 100) / 100 : null,
errorRateUnavailable: errorRate === null,
problemDetails: openProblems.map((p) => ({
id: p.displayId,
title: p.title,
duration: Math.round(p.durationMs / 60000) + ' minutes',
rootCause: p.rootCause?.name ?? 'investigating',
})),
};
}
Set the AliveMCP probe interval to 60 seconds — Dynatrace's Davis AI typically detects and opens problems within 30-60 seconds of symptom onset. A 60-second probe interval ensures AliveMCP reflects the Dynatrace problem state within two probe cycles of the problem appearing.
Related guides
- MCP tools for InfluxDB — Flux queries, write API, and bucket management
- MCP tools for Elasticsearch — Query DSL, cluster health, and index management
- MCP tools for Prometheus Alertmanager — alerts, silences, and inhibition rules
- MCP server observability — metrics, logs, and traces for production MCP tools
- SLO monitoring for MCP servers — error budgets, burn rate alerts, and policy
- MCP server health check patterns — composite endpoints and failure classification