Guide · AWS Audit & Compliance

MCP Server CloudTrail Insights — API anomaly detection, write rate spikes

CloudTrail Insights continuously analyzes your trail's write event rate and error rate against a learned 7-day baseline and fires an Insights event when either metric deviates significantly — providing automated anomaly detection for MCP tool abuse, runaway loops, and misconfigured tool handlers without writing a single custom metric. Three things trip teams up: Insights operates at the trail level, not the event selector level (you cannot scope Insights to a single DynamoDB table — it analyzes all write events flowing through the trail, so if you share a trail across many services the baseline absorbs their traffic too), the detection window is approximately 10 minutes (Insights is not a real-time alarm — a runaway tool handler executing 10,000 PutItems in 30 seconds will generate an Insights event in roughly 10 minutes, not seconds), and Insights events cost extra ($0.35 per 100,000 management events analyzed — separate from data event costs, and charged for all management write events analyzed whether or not an anomaly is found).

TL;DR

Enable Insights on the trail with InsightTypes: [ApiCallRateInsight, ApiErrorRateInsight]. Insights learns a 7-day rolling baseline of your write event rate. When the rate spikes >2× above baseline, an Insights START event fires to EventBridge. Subscribe a Lambda or SNS topic to the aws.cloudtrail event source with detail.insightType filter. Map the Insights event insightDetails.state to START/END for alert suppression. The 7-day learning period means Insights is useless on new trails — use CloudWatch metric filters on the CloudWatch Logs delivery for day-0 coverage instead.

How CloudTrail Insights works — the baseline and detection model

Insights uses a machine-learning model that observes the last 7 days of write events for each management API (CreateTable, PutItem at the management level, DeleteBucket, and so on) and establishes an expected hourly rate per API. When the observed rate in a 10-minute window exceeds the expected rate by a statistically significant margin, Insights emits a START event. When the rate returns to the baseline range, Insights emits an END event. The two event types together form a detection window you can use to alert on start and auto-resolve on end.

There are two Insight types as of 2026:

Crucially: Insights only analyzes management events, not data events. If your MCP tool is doing millions of PutItem calls (data events), Insights will not detect the spike. For data event anomaly detection you need CloudWatch Logs metric filters on the CloudWatch Logs delivery, or CloudTrail Lake SQL queries on a schedule.

Enabling Insights on an existing trail

# Enable both Insight types on existing trail
aws cloudtrail put-insight-selectors \
  --trail-name mcp-audit-trail \
  --insight-selectors '[
    {"InsightType": "ApiCallRateInsight"},
    {"InsightType": "ApiErrorRateInsight"}
  ]'

# Verify — should show both selectors in response
aws cloudtrail get-insight-selectors --trail-name mcp-audit-trail

In CDK, Insights is configured at trail construction time:

import * as cloudtrail from "aws-cdk-lib/aws-cloudtrail";

const trail = new cloudtrail.Trail(this, "McpAuditTrail", {
  trailName: "mcp-audit-trail",
  bucket: auditBucket,
  isMultiRegionTrail: true,
  enableFileValidation: true,
  managementEvents: cloudtrail.ReadWriteType.WRITE_ONLY,
  insightTypes: [
    cloudtrail.InsightType.API_CALL_RATE,
    cloudtrail.InsightType.API_ERROR_RATE,
  ],
});

After enabling Insights, CloudTrail requires 7 days of data before it can establish a reliable baseline. During the learning period, the console shows "Insufficient data" in the Insights tab. Insights events will start firing approximately 7 days after enablement. Do not rely on Insights for security alerting during this window.

CloudTrail Insights event structure

Insights events are delivered to the same S3 bucket as regular CloudTrail events but in a separate path: AWSLogs/<account>/CloudTrail-Insight/<region>/YYYY/MM/DD/. They are also forwarded to EventBridge from the aws.cloudtrail event source with detail-type: "AWS Insight via CloudTrail".

{
  "source": "aws.cloudtrail",
  "detail-type": "AWS Insight via CloudTrail",
  "detail": {
    "eventVersion": "1.08",
    "eventTime": "2026-09-19T15:30:00Z",
    "eventName": "PutItem",          // which API triggered the anomaly
    "eventSource": "dynamodb.amazonaws.com",
    "insightDetails": {
      "state": "Start",              // "Start" or "End"
      "eventSource": "dynamodb.amazonaws.com",
      "eventName": "PutItem",
      "insightType": "ApiCallRateInsight",
      "insightContext": {
        "statistics": {
          "baseline": {
            "average": 12.5          // expected calls per minute
          },
          "insight": {
            "average": 347.0         // observed calls per minute during anomaly
          }
        }
      }
    },
    "userIdentity": {
      "type": "AssumedRole",
      "principalId": "AROAEXAMPLEID:mcp-server-task",
      "arn": "arn:aws:sts::123456789012:assumed-role/mcp-server-task-role/mcp-server-task"
    }
  }
}

The insightDetails.statistics.baseline.average and insightDetails.statistics.insight.average values let you calculate the anomaly magnitude: 347/12.5 = 27.8× above baseline. This ratio is useful for severity triage: a 2× spike might be a legitimate traffic burst; a 27× spike during off-hours is more likely a bug or security incident.

EventBridge rule for Insights alerts

Insights events flow through EventBridge automatically — no extra configuration beyond enabling Insights on the trail. Create a rule to route them to SNS or Lambda:

import * as events from "aws-cdk-lib/aws-events";
import * as targets from "aws-cdk-lib/aws-events-targets";
import * as sns from "aws-cdk-lib/aws-sns";

const insightAlertTopic = new sns.Topic(this, "InsightAlerts", {
  topicName: "mcp-cloudtrail-insight-alerts",
});

// Route all Insights START events to SNS
new events.Rule(this, "InsightStartRule", {
  ruleName: "mcp-cloudtrail-insights-start",
  description: "Alert on CloudTrail Insights START events (anomalous API rate or error rate)",
  eventPattern: {
    source: ["aws.cloudtrail"],
    detailType: ["AWS Insight via CloudTrail"],
    detail: {
      insightDetails: {
        state: ["Start"],
        // Optional: scope to specific APIs your MCP server uses
        eventName: [
          "PutItem", "UpdateItem", "DeleteItem", "BatchWriteItem",
          "AssumeRole", "PutBucketPolicy", "CreateFunction20150331",
        ],
      },
    },
  },
  targets: [new targets.SnsTopic(insightAlertTopic, {
    message: events.RuleTargetInput.fromEventPath("$.detail"),
  })],
});

// Separate rule to auto-resolve (for incident management systems that support it)
new events.Rule(this, "InsightEndRule", {
  ruleName: "mcp-cloudtrail-insights-end",
  eventPattern: {
    source: ["aws.cloudtrail"],
    detailType: ["AWS Insight via CloudTrail"],
    detail: {
      insightDetails: { state: ["End"] },
    },
  },
  targets: [new targets.SnsTopic(insightAlertTopic)],
});

Lambda handler: severity triage and Slack alert

Rather than sending raw Insights events to an on-call pager, triage by anomaly magnitude before alerting:

import { CloudTrailInsightEvent } from "./types";

const SLACK_WEBHOOK_URL = process.env.SLACK_WEBHOOK_URL!;
const HIGH_SEVERITY_RATIO = 10; // >10× baseline = high severity
const MEDIUM_SEVERITY_RATIO = 3; // 3-10× baseline = medium

export async function handler(event: CloudTrailInsightEvent) {
  const details = event.detail.insightDetails;
  if (details.state !== "Start") return; // ignore End events here; handled separately

  const baselineRate = details.insightContext.statistics.baseline.average;
  const observedRate = details.insightContext.statistics.insight.average;
  const ratio = observedRate / (baselineRate || 1);

  const severity = ratio >= HIGH_SEVERITY_RATIO ? "HIGH"
    : ratio >= MEDIUM_SEVERITY_RATIO ? "MEDIUM" : "LOW";

  const emoji = severity === "HIGH" ? "🚨" : severity === "MEDIUM" ? "⚠️" : "ℹ️";

  const message = {
    text: `${emoji} *CloudTrail Insights anomaly* — ${severity}`,
    attachments: [{
      color: severity === "HIGH" ? "danger" : severity === "MEDIUM" ? "warning" : "good",
      fields: [
        { title: "API", value: `${details.eventSource} / ${details.eventName}`, short: true },
        { title: "Insight type", value: details.insightType, short: true },
        { title: "Baseline rate", value: `${baselineRate.toFixed(1)}/min`, short: true },
        { title: "Observed rate", value: `${observedRate.toFixed(1)}/min (${ratio.toFixed(1)}×)`, short: true },
        { title: "Principal", value: event.detail.userIdentity?.principalId ?? "unknown", short: false },
        { title: "Time", value: event.detail.eventTime, short: true },
      ],
    }],
  };

  await fetch(SLACK_WEBHOOK_URL, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(message),
  });
}

Limitations and when not to use Insights

10-minute detection latency: Insights is not suitable for real-time security response. A compromised MCP tool calling DeleteBucket 100 times in 60 seconds will generate an Insights START event roughly 10 minutes later. For sub-minute detection, use CloudWatch metric filters on CloudTrail → CloudWatch Logs delivery with an alarm on the DeleteBucket event count.

Management events only: As noted above, DynamoDB data-plane events (PutItem, GetItem) are not analyzed by Insights. A runaway MCP loop hammering DynamoDB at 10,000 writes/sec will not trigger an Insights event — use CloudWatch custom metrics (increment a counter in your tool handler) or CloudTrail Lake scheduled queries for data-plane anomaly detection.

No per-resource scoping: Insights looks at the whole trail, not individual resources. If your trail covers a shared account with many teams, Insights baselines reflect all teams' combined write rates. An anomaly in another team's service will fire an alert indistinguishable from an MCP server anomaly. Use dedicated trails per service or dedicated accounts to keep Insights baselines clean.

No custom threshold configuration: The anomaly threshold is determined automatically by the ML model. You cannot set "alert me when PutItem rate exceeds 1,000/min." For threshold-based alerting, use CloudWatch metric filters.

Cost on busy trails: Insights charges $0.35 per 100,000 management write events analyzed. A trail receiving 10 million management write events per month costs $35/month in Insights fees alone, on top of the trail storage fees. Evaluate whether the automated baseline learning justifies the cost versus hand-tuned CloudWatch alarms.

# Day-0 alternative: CloudWatch metric filter on CloudTrail Logs delivery
# (works before 7-day baseline is established)
# Requires: trail with CloudWatch Logs delivery enabled

aws logs put-metric-filter \
  --log-group-name "mcp-cloudtrail-logs" \
  --filter-name "AssumeRoleCount" \
  --filter-pattern '{ $.eventName = "AssumeRole" && $.errorCode NOT EXISTS }' \
  --metric-transformations \
    metricName=AssumeRoleSuccessCount,metricNamespace=MCP/CloudTrail,metricValue=1,defaultValue=0

# Alarm when AssumeRole rate exceeds 100/min (potential credential harvesting)
aws cloudwatch put-metric-alarm \
  --alarm-name "mcp-AssumeRole-spike" \
  --metric-name AssumeRoleSuccessCount \
  --namespace MCP/CloudTrail \
  --statistic Sum \
  --period 60 \
  --evaluation-periods 3 \
  --threshold 100 \
  --comparison-operator GreaterThanThreshold \
  --alarm-actions "arn:aws:sns:us-east-1:123456789012:mcp-security-alerts"

Failure modes

SymptomRoot causeFix
No Insights events after enabling — "Insufficient data" in console Trail has less than 7 days of data — baseline not yet established Wait 7 days; use CloudWatch metric filters for immediate coverage
Insights fires constantly on normal traffic patterns High traffic variability (batch jobs, scheduled tasks) makes baseline noisy Use dedicated trail for MCP server only; separate high-variability batch workloads onto a different trail
Insights event in EventBridge but Lambda not invoked EventBridge rule pattern does not match — detail field path wrong Test with aws events put-events --entries '...' and verify insightDetails.state path in rule pattern; Insights uses detail.insightDetails.state, not detail.state
Both START and END events firing for the same anomaly triggering duplicate alerts Lambda handling both event types without filtering by state Check details.state === "Start" before alerting; route END events to a separate resolution handler
Insights not detecting known runaway tool handler (high PutItem rate) PutItem is a data event, not a management event — Insights does not analyze data events Use CloudTrail Lake scheduled query or CloudWatch custom metric from application instrumentation