Guide · Lambda Powertools
AWS Lambda Powertools Tracer for MCP Servers
AWS Lambda Powertools Tracer wraps the AWS X-Ray SDK to give you distributed tracing across MCP server Lambda functions with minimal boilerplate. For MCP servers, distributed tracing is critical: a single tool call may invoke 3–5 Lambda functions in sequence (auth check → tool dispatcher → external API adapter → result formatter → audit logger), and without traces you have no way to know which step added 800 ms to what should be a 200 ms call. Three things teams routinely misconfigure: leaving POWERTOOLS_TRACER_CAPTURE_RESPONSE=true (the default) on high-throughput handlers — for MCP tools that return large documents or code snippets, capturing the full response in every trace segment can push segment sizes past X-Ray's 64 KB limit, silently truncating data; annotating with high-cardinality values like user_id when X-Ray charges per annotation and enforces limits; not adding the active tracing IAM permission, causing the tracer to silently fall back to passthrough mode and record nothing.
TL;DR
Decorate handlers with @tracer.capture_lambda_handler(capture_response=False) and internal methods with @tracer.capture_method. Use tracer.put_annotation for 2–3 low-cardinality fields (tool name, status, tier). Use tracer.put_metadata for large structured data. Set POWERTOOLS_TRACE_DISABLED=true in local/test environments.
Tracer setup and IAM requirements
Tracer requires the Lambda execution role to have xray:PutTraceSegments and xray:PutTelemetryRecords permissions, and the function must have Active Tracing enabled (not just Pass-Through). Without active tracing, X-Ray receives no data and the tracer silently does nothing — it does not raise an error.
from aws_lambda_powertools import Tracer
from aws_lambda_powertools.utilities.typing import LambdaContext
tracer = Tracer(service="mcp-tool-dispatcher")
@tracer.capture_lambda_handler(
capture_response=False, # don't store response in segment — large MCP outputs
capture_error=True, # capture exception details on failure (default True)
)
def handler(event: dict, context: LambdaContext) -> dict:
tool_name = event.get("toolName", "unknown")
tracer.put_annotation(key="ToolName", value=tool_name)
tracer.put_annotation(key="UserTier", value=event.get("userTier", "free"))
return dispatch_tool(event)
CloudFormation / CDK active tracing configuration:
# CDK (TypeScript)
const fn = new lambda.Function(this, "McpToolDispatcher", {
tracing: lambda.Tracing.ACTIVE, // required — PASS_THROUGH records nothing
// ...
});
fn.addToRolePolicy(new iam.PolicyStatement({
actions: ["xray:PutTraceSegments", "xray:PutTelemetryRecords"],
resources: ["*"],
}));
capture_method — tracing internal operations
@tracer.capture_method creates an X-Ray subsegment for any method in your MCP server code. The subsegment appears as a child of the Lambda handler segment in the service map, with its own start time, end time, and error state. This is how you identify which internal step is slow — not just that the Lambda was slow.
from aws_lambda_powertools import Tracer
tracer = Tracer(service="mcp-tool-dispatcher")
@tracer.capture_method
def call_external_api(endpoint: str, payload: dict) -> dict:
# This method appears as a subsegment named "call_external_api"
# Duration and any exception are captured automatically
response = http_client.post(endpoint, json=payload)
response.raise_for_status()
return response.json()
@tracer.capture_method
def validate_mcp_input(tool_name: str, input_data: dict) -> bool:
schema = load_tool_schema(tool_name)
return schema.validate(input_data)
For async methods, Powertools Tracer supports async functions natively:
@tracer.capture_method
async def fetch_context_async(session_id: str) -> dict:
async with aiohttp.ClientSession() as session:
async with session.get(f"{context_store_url}/{session_id}") as resp:
return await resp.json()
Annotations vs metadata
X-Ray has two mechanisms for attaching data to segments. They look similar but have very different behavior:
| Feature | Annotations | Metadata |
|---|---|---|
| Searchable in X-Ray console | Yes — indexed | No — not indexed |
| Filterable in GetTraceSummaries API | Yes | No |
| Value types | String, number, boolean only | Any JSON-serialisable value |
| Size limit | 50 annotations per segment, value ≤ 1,000 chars | Segment total ≤ 64 KB |
| Cost | No extra charge, but indexing has limits | No extra charge |
| Use for | Low-cardinality filter fields: ToolName, Status, UserTier | Large structured data: full request body, schema info, debug state |
# Annotations: searchable, low-cardinality
tracer.put_annotation(key="ToolName", value=tool_name) # "search_codebase"
tracer.put_annotation(key="Status", value="success") # "success" | "error"
tracer.put_annotation(key="UserTier", value="pro") # "free" | "pro" | "enterprise"
# Metadata: not searchable, use for debugging context
tracer.put_metadata(key="tool_input", value={"query": query, "filters": filters})
tracer.put_metadata(key="api_response_headers", value=dict(response.headers))
A common mistake is annotating with user_id or request_id. These are high-cardinality: X-Ray won't index them usefully for filtering, and you hit the 50-annotation limit quickly. Use metadata for these values — they're still visible in individual trace views but won't pollute the index.
Manual subsegments for fine-grained timing
When you can't use @tracer.capture_method (e.g., inside a loop, or for a code block that isn't a standalone function), use the context manager form:
from aws_lambda_powertools import Tracer
tracer = Tracer(service="mcp-tool-dispatcher")
def process_tool_batch(tool_calls: list) -> list:
results = []
for call in tool_calls:
# Create a named subsegment for each iteration
with tracer.provider.in_subsegment(f"tool-{call['name']}") as subsegment:
subsegment.put_annotation("ToolName", call["name"])
try:
result = execute_single_tool(call)
subsegment.put_annotation("Status", "success")
results.append(result)
except Exception as e:
subsegment.put_annotation("Status", "error")
subsegment.add_exception(e, traceback.extract_stack())
raise
return results
Disabling tracer in local and test environments
X-Ray SDK raises errors when run outside a Lambda environment without an active trace context. Powertools Tracer disables itself automatically when:
POWERTOOLS_TRACE_DISABLED=trueis setAWS_SAM_LOCAL=trueis set (SAM CLI sets this automatically)- The function runs outside Lambda (no
_X_AMZN_TRACE_IDenv var)
When disabled, all decorator and method calls are no-ops — no exceptions, no code changes needed between environments. In unit tests this means you can test handler logic without mocking X-Ray SDK at all:
# pytest conftest.py — disable tracer for all unit tests
import os
os.environ["POWERTOOLS_TRACE_DISABLED"] = "true"
os.environ["POWERTOOLS_SERVICE_NAME"] = "test-service"
Common failures
| Symptom | Root cause | Fix |
|---|---|---|
| No traces appear in X-Ray console | Function has Pass-Through tracing, not Active; or missing IAM permissions | Set Tracing: Active in template; add xray:PutTraceSegments to execution role |
| Segment data is truncated, partial traces only | Response capture (capture_response=True) exceeds 64 KB segment limit |
Set capture_response=False on capture_lambda_handler |
SegmentNotFoundException in unit tests |
X-Ray SDK can't find active segment — not in Lambda environment | Set POWERTOOLS_TRACE_DISABLED=true in test env vars |
| Subsegments appear but contain no annotation data | put_annotation called before entering the subsegment context |
Call put_annotation inside the with tracer.provider.in_subsegment() block, or after capture_lambda_handler entry |
| Traces show 100% error rate despite successful calls | capture_error=True capturing expected business errors (e.g., input validation failures) as X-Ray errors |
Set capture_error=False and manually call subsegment.add_error() only for unexpected exceptions |
Trace gaps in your MCP server availability
X-Ray traces show you latency distribution inside Lambda. AliveMCP shows you when the entire MCP server endpoint goes dark — uptime monitoring with instant Slack alerts and a public status page.
Join the waitlist →