Guide · AWS Core Services
MCP Server EventBridge — event routing, scheduled rules, custom event buses
Amazon EventBridge is an event router: your MCP server publishes events to a bus, rules match those events by pattern, and matched events are forwarded to targets (SQS, Lambda, Step Functions, API Gateway, another bus). EventBridge's key advantage over SNS is content-based routing via event patterns — rules can match on any field of the event JSON, not just header attributes. The three things that distinguish EventBridge from SNS/SQS are: events are JSON objects with a fixed envelope (source, detail-type, detail are required fields — missing them causes silent event drops), the default event bus receives AWS service events (EC2 state changes, S3 object events, Secrets Manager rotation — use custom buses for your own events), and EventBridge Scheduler replaces CloudWatch scheduled rules for one-time and recurring invocations with per-schedule IAM roles.
TL;DR
Install @aws-sdk/client-eventbridge. Use PutEventsCommand to publish events from MCP tools. Set Source, DetailType, and Detail on every event entry — missing any of the three causes the event to be dropped with no error. Use custom event buses (not the default bus) for application events. Use EventBridge Scheduler for time-based tool triggering without a cron Lambda.
Publishing events: the event envelope
Every EventBridge event has a fixed schema. The Detail field is a JSON-serialised string (not an object). Source is a free-form namespace (convention: reverse-domain like com.myapp.mcp). DetailType is a human-readable event name.
import { EventBridgeClient, PutEventsCommand } from "@aws-sdk/client-eventbridge";
const eb = new EventBridgeClient({ region: process.env.AWS_REGION ?? "us-east-1" });
const EVENT_BUS_NAME = process.env.EVENTBRIDGE_BUS_NAME ?? "default";
async function publishEvent(detailType: string, detail: object) {
const result = await eb.send(new PutEventsCommand({
Entries: [{
EventBusName: EVENT_BUS_NAME,
Source: "com.myapp.mcp", // REQUIRED — missing = event silently dropped
DetailType: detailType, // REQUIRED — the event "type" for rule matching
Detail: JSON.stringify(detail), // REQUIRED — must be a JSON string, not an object
Time: new Date(), // optional — defaults to current time
}],
}));
// PutEvents returns HTTP 200 even on partial failure
// Check FailedEntryCount and Entries[].ErrorCode
if (result.FailedEntryCount && result.FailedEntryCount > 0) {
const failure = result.Entries?.find(e => e.ErrorCode);
throw new Error(`EventBridge publish failed: ${failure?.ErrorMessage}`);
}
return result.Entries?.[0]?.EventId;
}
PutEvents returns HTTP 200 even when individual entries fail. Always check FailedEntryCount — a non-zero value means at least one entry was rejected. Each entry in the response array has an ErrorCode and ErrorMessage for failed entries. The maximum batch size is 10 entries per PutEvents call.
Custom event buses: isolating application events
The default event bus receives AWS service events (EC2 state changes, CloudTrail API calls, Secrets Manager rotation events). Publishing application events to the default bus mixes them with AWS service events, making rule debugging harder and increasing the risk of an application rule accidentally matching an AWS service event.
// AWS CDK: custom event bus
import * as events from "aws-cdk-lib/aws-events";
import * as targets from "aws-cdk-lib/aws-events-targets";
import * as sqs from "aws-cdk-lib/aws-sqs";
const bus = new events.EventBus(this, "AppEventBus", {
eventBusName: "mcp-app-events",
});
// Rule: route job completion events to an SQS queue
const jobDoneQueue = new sqs.Queue(this, "JobDoneQueue");
new events.Rule(this, "JobCompletedRule", {
eventBus: bus,
ruleName: "job-completed",
eventPattern: {
source: ["com.myapp.mcp"],
detailType: ["JobCompleted"],
// Match on detail fields — content-based routing
detail: {
status: ["success"],
priority: [{ numeric: [">=", 2] }],
},
},
targets: [new targets.SqsQueue(jobDoneQueue)],
});
Event patterns support prefix matching ({ "prefix": "user." }), numeric ranges ({ "numeric": [">=", 0, "<=", 100] }), and the anything-but operator. Patterns are evaluated against the event JSON and must match the envelope structure exactly — detail matches fields inside the Detail JSON, not the envelope-level Detail string.
Scheduled rules: triggering MCP tools on a schedule
EventBridge rules support cron and rate expressions for time-based invocation. Rate expressions (rate(5 minutes)) are simple recurring triggers. Cron expressions (cron(0 9 * * ? *)) support specific times — note that EventBridge cron uses ? for day-of-week or day-of-month (not both can be specified), and all times are UTC.
// AWS CDK: scheduled rule that invokes a Lambda (which calls your MCP server)
import * as events from "aws-cdk-lib/aws-events";
import * as targets from "aws-cdk-lib/aws-events-targets";
import * as lambda from "aws-cdk-lib/aws-lambda";
const scheduledFn = lambda.Function.fromFunctionArn(this, "McpInvoker",
process.env.MCP_INVOKER_LAMBDA_ARN!);
// Rate expression — runs every 15 minutes
new events.Rule(this, "PeriodicCheck", {
schedule: events.Schedule.rate(cdk.Duration.minutes(15)),
targets: [new targets.LambdaFunction(scheduledFn, {
event: events.RuleTargetInput.fromObject({
tool: "check_endpoints",
args: { region: "us-east-1" },
}),
})],
});
// Cron expression — runs at 9am UTC on weekdays
new events.Rule(this, "DailyReport", {
schedule: events.Schedule.cron({
minute: "0",
hour: "9",
weekDay: "MON-FRI", // day-of-month must be ? when day-of-week is specified
}),
targets: [new targets.LambdaFunction(scheduledFn, {
event: events.RuleTargetInput.fromObject({ tool: "send_daily_report" }),
})],
});
EventBridge scheduled rules invoke targets with at-least-once semantics — a rule may fire twice within a short window during rare edge cases. Make your target Lambda (or MCP tool handler) idempotent by tracking which scheduled invocation has already been processed.
EventBridge Scheduler: per-schedule IAM and one-time invocations
EventBridge Scheduler is a newer service that extends scheduled rules with per-schedule IAM roles (the scheduler assumes the role to invoke the target), flexible window delivery (invoke within a ±N minute window to spread load), and one-time scheduled invocations (trigger exactly once at a future datetime, then auto-delete).
import { SchedulerClient, CreateScheduleCommand } from "@aws-sdk/client-scheduler";
const scheduler = new SchedulerClient({ region: process.env.AWS_REGION ?? "us-east-1" });
// One-time invocation: trigger an MCP tool exactly once at a future time
await scheduler.send(new CreateScheduleCommand({
Name: `run-report-${Date.now()}`,
GroupName: "mcp-schedules",
ScheduleExpression: "at(2026-09-01T09:00:00)", // ISO 8601 datetime, UTC
ScheduleExpressionTimezone: "UTC",
ActionAfterCompletion: "DELETE", // auto-delete the schedule after it fires once
FlexibleTimeWindow: { Mode: "OFF" },
Target: {
Arn: process.env.TARGET_LAMBDA_ARN!,
RoleArn: process.env.SCHEDULER_ROLE_ARN!, // role Scheduler assumes to invoke target
Input: JSON.stringify({ tool: "generate_report", period: "2026-08" }),
},
}));
// Recurring schedule: every day at 6am UTC
await scheduler.send(new CreateScheduleCommand({
Name: "daily-health-check",
ScheduleExpression: "cron(0 6 * * ? *)",
ScheduleExpressionTimezone: "UTC",
FlexibleTimeWindow: { Mode: "FLEXIBLE", MaximumWindowInMinutes: 5 },
Target: {
Arn: process.env.TARGET_LAMBDA_ARN!,
RoleArn: process.env.SCHEDULER_ROLE_ARN!,
Input: JSON.stringify({ tool: "check_all_endpoints" }),
},
}));
Receiving EventBridge events in an MCP tool
An MCP server can expose a tool that queries an SQS queue backed by EventBridge, or expose a tool that lists recent events from a DynamoDB table populated by an EventBridge-triggered Lambda. Direct HTTP delivery from EventBridge to an MCP server endpoint is also possible via the API Gateway target type.
// Pattern: EventBridge → SQS → MCP poll tool
// The MCP tool reads from SQS, which EventBridge delivers to
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import { SQSClient, ReceiveMessageCommand, DeleteMessageCommand } from "@aws-sdk/client-sqs";
const server = new McpServer({ name: "event-consumer", version: "1.0.0" });
const sqsClient = new SQSClient({ region: process.env.AWS_REGION ?? "us-east-1" });
server.tool(
"poll_events",
"Pull up to 10 pending events from the EventBridge-backed SQS queue",
{ max_events: z.number().int().min(1).max(10).default(10) },
async ({ max_events }) => {
const response = await sqsClient.send(new ReceiveMessageCommand({
QueueUrl: process.env.EVENT_QUEUE_URL!,
MaxNumberOfMessages: max_events,
WaitTimeSeconds: 5,
}));
const events = (response.Messages ?? []).map(msg => {
const envelope = JSON.parse(msg.Body!); // EventBridge envelope
return {
id: envelope.id,
source: envelope.source,
detailType: envelope["detail-type"],
detail: JSON.parse(envelope.detail),
time: envelope.time,
receiptHandle: msg.ReceiptHandle,
};
});
return { content: [{ type: "text", text: JSON.stringify(events) }] };
}
);
IAM policy for EventBridge publish
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["events:PutEvents"],
"Resource": "arn:aws:events:us-east-1:123456789012:event-bus/mcp-app-events"
}
]
}
// For Scheduler: the scheduler role needs permission to invoke the target
// Scheduler execution role:
{
"Statement": [{
"Effect": "Allow",
"Action": ["lambda:InvokeFunction"],
"Resource": "arn:aws:lambda:us-east-1:123456789012:function:mcp-invoker"
}]
}
Common failure modes
| Symptom | Cause | Fix |
|---|---|---|
| Event published but rule never fires | Source or DetailType in rule pattern doesn't match event | Use EventBridge test event patterns in the console; check for case sensitivity |
| PutEvents returns 200 but event not delivered | FailedEntryCount not checked; individual entry failed | Always inspect result.FailedEntryCount and per-entry ErrorCode |
| Detail field parse error downstream | Detail set as object instead of JSON string | Always JSON.stringify() the Detail value before setting it |
| Scheduled rule doesn't trigger | Rule state is DISABLED; or target IAM role missing | Check rule state in console; ensure target has appropriate IAM permissions |
| EventBridge Scheduler fires twice | At-least-once semantics with flexible window overlap | Implement idempotency key in target using schedule name + fire time |
| Cross-account events not received | Receiving account event bus resource policy not updated | Add events:PutEvents permission to receiving bus resource policy for source account |