Guide · AWS Observability
MCP Server X-Ray Service Map — dependency visualization, CloudWatch ServiceLens, edge health
The AWS X-Ray service map is a graph of every service your MCP server calls, with each edge annotated by request rate, error rate, and latency percentiles. Three things matter for interpreting it correctly: error vs fault vs throttle are distinct (error = 4xx client error from downstream, fault = 5xx server error from downstream, throttle = 429 or equivalent rate-limit — a DynamoDB ProvisionedThroughputExceeded is a throttle, not an error); service map nodes are named by the segment name set in your code (if two ECS services both call openSegment("backend"), they collapse into one node); and the map reflects only sampled traffic (if your sampling rate is 5% and DynamoDB is only hit by 10% of requests, the DynamoDB edge may appear empty or missing for short time windows where the intersection of samples is zero).
TL;DR
The service map appears automatically once traces contain AWS SDK subsegments (via captureAWSv3Client) or remote calls. Give each microservice a unique segment name. Use CloudWatch ServiceLens to link service map edges to CloudWatch metrics (request count, latency, error rate) on the same time axis. Query the service graph programmatically with GetServiceGraph to build custom dashboards or feed alerts. An edge showing 100% error rate with <5 requests/min usually means the sampling rate is too low to represent the actual traffic.
Service map topology for a typical MCP server
A fully instrumented MCP server produces a service map with nodes for: the MCP server itself, every downstream AWS service it calls (DynamoDB, S3, ElastiCache, SES), every external API it calls (if instrumented with manual subsegments), and the ALB or Lambda URL if requests enter through those. Edges show the health of each call relationship.
// Service map node types produced by a typical MCP server:
//
// [client] — remote calling client (no agent running here)
// │ edge: request_rate, latency, error%, fault%, throttle%
// ▼
// [mcp-server] ← your ECS/Lambda service (named by openSegment("mcp-server"))
// │
// ├── [DynamoDB] ← auto via captureAWSv3Client; TableName in subsegment
// │ edge: latency, throttle% (ProvisionedThroughputExceeded shows here)
// │
// ├── [S3] ← auto via captureAWSv3Client; Bucket in subsegment
// │
// ├── [ElastiCache] ← manual subsegment (no SDK auto-instrumentation for ElastiCache)
// │ sub.addRemoteRequestData("elasticache.us-east-1.amazonaws.com", 6379, true)
// │
// └── [external-api] ← manual subsegment for OpenAI / Anthropic / etc.
//
// Node health indicators (concentric ring colors in X-Ray console):
// Green = ok requests
// Yellow = errors (4xx from downstream)
// Red = faults (5xx from downstream)
// Purple = throttles (429 / ProvisionedThroughputExceeded)
Ensuring distinct service map nodes
X-Ray groups nodes by (segment name, account ID, region). If two different services use the same segment name, they merge into one node — making the map look like one service with unexpectedly high call volume.
// BAD: two services with the same name collapse into one map node
// Service A (auth server):
AWSXRay.express.openSegment("backend");
// Service B (MCP server):
AWSXRay.express.openSegment("backend"); // same name → same node in service map
// GOOD: unique names per service
AWSXRay.express.openSegment("mcp-server-prod");
AWSXRay.express.openSegment("auth-server-prod");
// For Lambda: the function name is used as the segment name automatically.
// You still call setSegmentName() or use naming strategies for ECS/EC2.
// Dynamic naming strategy: vary segment name by environment
const serviceName = `mcp-server-${process.env.ENVIRONMENT ?? "local"}`;
// → "mcp-server-prod", "mcp-server-staging", "mcp-server-local"
// Creates separate nodes per environment — useful for side-by-side comparison
// but can clutter the service map if too many environments run simultaneously.
// Fixed naming strategy (recommended for production):
AWSXRay.middleware.setDefaultName("mcp-server");
// All ECS tasks running this service appear as one node with aggregate health.
Annotating remote calls not covered by SDK auto-instrumentation
ElastiCache, managed Kafka/MSK, and external HTTP APIs are not automatically instrumented by the AWS SDK patch. Use addRemoteRequestData to make them appear as properly typed edges in the service map rather than as generic "Remote call" nodes.
import AWSXRay from "aws-xray-sdk-node";
import { createClient } from "redis";
// Redis / ElastiCache — manual service map edge
async function tracedRedisGet(key: string): Promise {
const segment = AWSXRay.resolveSegment();
const sub = segment?.addNewSubsegment("Redis");
sub?.addRemoteRequestData(
process.env.REDIS_HOST ?? "localhost", // hostname
parseInt(process.env.REDIS_PORT ?? "6379"), // port
true // is remote (not local)
);
// addRemoteRequestData sets the subsegment's namespace to "remote"
// which makes it render as an external dependency edge in the service map
try {
const value = await redisClient.get(key);
sub?.close();
return value;
} catch (err) {
sub?.addError(err as Error);
sub?.close();
return null;
}
}
// External API (OpenAI, Anthropic) — manual service map edge
async function tracedExternalApi(host: string, path: string, fn: () => Promise) {
const segment = AWSXRay.resolveSegment();
const sub = segment?.addNewSubsegment(host); // node name in service map
sub?.addRemoteRequestData(host, 443, true);
try {
const resp = await fn();
sub?.addAnnotation("http_status", resp.status);
if (!resp.ok) {
resp.status >= 500 ? sub?.addFaultFlag() : sub?.addErrorFlag();
if (resp.status === 429) sub?.addThrottleFlag();
}
sub?.close();
return resp;
} catch (err) {
sub?.addError(err as Error);
sub?.close();
throw err;
}
}
CloudWatch ServiceLens integration
CloudWatch ServiceLens overlays CloudWatch metrics onto the X-Ray service map, giving you request count, latency, and error rate on the same dashboard without switching tools. ServiceLens synthesizes metrics from the X-Ray traces automatically — no additional instrumentation needed.
// ServiceLens is available in the CloudWatch console under "Application Monitoring → ServiceLens"
// It reads X-Ray traces and synthesizes CloudWatch metrics per service:
// - RequestCount: total requests per minute to the service
// - Latency (p50, p90, p99): from trace durations
// - FaultRate: % of requests with fault (5xx)
// - ErrorRate: % of requests with error (4xx)
//
// These are surfaced as CloudWatch metrics under the namespace "AWS/X-Ray"
// (note: different from custom X-Ray group metrics)
// To add your own application metrics alongside ServiceLens auto-metrics:
// Instrument CloudWatch PutMetricData in tool handlers AND enable X-Ray tracing.
// ServiceLens can link from a trace to the CloudWatch dashboard showing the
// same time window — the "View in CloudWatch" link uses the trace timestamp
// to pre-fill the CloudWatch dashboard time range.
// ServiceLens alarm integration:
import { CloudWatchClient, PutMetricAlarmCommand } from "@aws-sdk/client-cloudwatch";
// Alarm on p99 latency derived from ServiceLens metrics
await cw.send(new PutMetricAlarmCommand({
AlarmName: "McpServer-P99-Latency",
Namespace: "AWS/X-Ray",
MetricName: "ResponseTime",
Dimensions: [{ Name: "ServiceName", Value: "mcp-server" }],
ExtendedStatistic: "p99", // percentile alarm — requires ExtendedStatistic not Statistic
Period: 300, // 5-minute evaluation period
Threshold: 2.0, // 2 seconds p99 latency
ComparisonOperator: "GreaterThanThreshold",
EvaluationPeriods: 2,
TreatMissingData: "notBreaching",
}));
Querying the service graph via API
The GetServiceGraph API returns the service map as a JSON graph, suitable for building custom dashboards, feeding alert logic, or comparing service maps across time windows (before/after a deployment).
import { XRayClient, GetServiceGraphCommand } from "@aws-sdk/client-xray";
const xray = new XRayClient({ region: process.env.AWS_REGION ?? "us-east-1" });
// Fetch the service graph for the last 5 minutes
const endTime = new Date();
const startTime = new Date(endTime.getTime() - 5 * 60 * 1000);
const response = await xray.send(new GetServiceGraphCommand({
StartTime: startTime,
EndTime: endTime,
// GroupName: "mcp-tool-errors" // optional: restrict to a specific group
}));
// response.Services: array of service nodes
for (const service of response.Services ?? []) {
console.log({
name: service.Name, // e.g., "mcp-server", "DynamoDB"
type: service.Type, // e.g., "AWS::ECS::Container", "AWS::DynamoDB::Table"
// SummaryStatistics: request counts, error/fault/throttle counts, latency percentiles
requestCount: service.SummaryStatistics?.TotalCount,
faultRate: (
(service.SummaryStatistics?.FaultStatistics?.TotalCount ?? 0) /
(service.SummaryStatistics?.TotalCount ?? 1)
).toFixed(3),
p99_latency: service.DurationHistogram?.find(h => h.Value === 0.99)?.Count,
// Edges: downstream services this node calls
edges: (service.Edges ?? []).map(e => ({
targetName: e.TargetServiceId,
responseTime: e.ResponseTimeHistogram,
})),
});
}
// Use case: compare fault rates before/after a deployment
// Fetch graph for [deployTime - 30min, deployTime] and [deployTime, deployTime + 30min]
// Compare SummaryStatistics.FaultStatistics per service to detect regressions
Common failure modes
| Symptom | Cause | Fix |
|---|---|---|
| DynamoDB or S3 node missing from service map | AWS SDK not patched with captureAWSv3Client, or no sampled traces in the selected time window | Confirm captureAWSv3Client wraps each SDK client; widen time window; increase sampling rate temporarily |
| Two distinct services collapsed into one node | Both services use identical segment names (e.g., both call openSegment("server")) | Use unique names per service; include environment suffix ("mcp-server-prod") |
| Service map edge shows 100% fault rate but monitoring shows service is healthy | Very low sample count on that edge — 1 fault out of 1 sampled request = 100% | Increase sampling rate for the relevant service or time window; check request count on the edge tooltip |
| ElastiCache or external APIs not shown as named nodes | No X-Ray subsegments for those calls; they appear as part of the parent service | Add manual subsegments with addRemoteRequestData for each non-AWS-SDK dependency |
| ServiceLens latency metrics don't match application metrics | ServiceLens computes latency from X-Ray trace durations, which include SDK overhead; application metrics may measure only handler logic | This is expected — ServiceLens includes network and SDK time; use application metrics for pure handler latency and ServiceLens for end-to-end latency |
GetServiceGraph returns empty Services array | No traces exist in the specified time window, or group filter expression matches zero traces | Verify traces exist in the X-Ray console for the same time range; remove group filter to see all traces first |