Guide · Event-Driven & Async Patterns
MCP Server AWS EventBridge — PutEvents, event patterns, buses, schema registry
Three EventBridge behaviours catch MCP server authors off-guard: EventBridge delivery is fire-and-forget — your tool cannot get a response from a target Lambda or Step Function — when you call PutEvents, EventBridge accepts the event and returns a success immediately; what happens downstream (which Lambda ran, whether it succeeded or failed) is not returned to the caller; MCP tools that need to confirm downstream effects must query the target system directly or poll a shared state store rather than waiting for a PutEvents response; event pattern matching is partial JSON matching — a pattern only checks the fields it specifies — an EventBridge pattern like {"source": ["myapp"], "detail-type": ["order.created"]} matches any event with those exact source and detail-type values regardless of what other fields are present; the match is always inclusion-based; this means an agent tool that describes an event's structure does not need to enumerate every field in the filter pattern; and the default event bus receives AWS service events automatically, but custom buses receive only events your application puts there — many first-time users call PutEvents to the default bus for application events and are surprised that AWS CloudTrail events appear alongside their own; always create a custom event bus (one per bounded context) to keep event streams isolated and simplify IAM policies.
TL;DR
Use @aws-sdk/client-eventbridge. Publish with PutEventsCommand — always set Source (reverse-DNS style, e.g. com.example.orders), DetailType (human-readable event name), and Detail (JSON string). Create a custom event bus per domain — never put application events on the default bus. List rules with ListRulesCommand, describe targets with ListTargetsByRuleCommand. Use EventBridge Schema Registry for JSON Schema validation before publishing. Check FailedEntryCount in the PutEvents response — a 200 with FailedEntryCount: 1 means your event was rejected.
Publishing events with PutEvents
EventBridge accepts up to 10 events per PutEvents call, each up to 256 KB. The response's FailedEntryCount field is the most important field to check — EventBridge returns HTTP 200 even when individual entries fail (e.g., invalid JSON in Detail, missing required fields, or ARN not found). If FailedEntryCount is greater than zero, inspect the Entries array — failed entries have an ErrorCode and ErrorMessage; successful entries have an EventId.
import {
EventBridgeClient,
PutEventsCommand,
ListRulesCommand,
ListTargetsByRuleCommand,
DescribeEventBusCommand,
} from '@aws-sdk/client-eventbridge';
import { z } from 'zod';
const eb = new EventBridgeClient({
region: process.env.AWS_REGION ?? 'us-east-1',
// Credentials from env (AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY)
// or EC2/ECS/Lambda instance role (recommended for production)
});
const EVENT_BUS_NAME = process.env.EVENTBRIDGE_BUS_NAME ?? 'default';
// MCP tool — publish one or more domain events
server.tool(
'publish_events',
{
events: z.array(z.object({
source: z.string().min(1), // e.g. 'com.example.orders'
detail_type: z.string().min(1), // e.g. 'order.created'
detail: z.record(z.unknown()),
resources: z.array(z.string()).optional(), // e.g. ['arn:aws:s3:::my-bucket/orders/123']
})).min(1).max(10),
},
async ({ events }) => {
const command = new PutEventsCommand({
Entries: events.map(e => ({
EventBusName: EVENT_BUS_NAME,
Source: e.source,
DetailType: e.detail_type,
Detail: JSON.stringify(e.detail),
Resources: e.resources,
Time: new Date(),
})),
});
const response = await eb.send(command);
if ((response.FailedEntryCount ?? 0) > 0) {
const failures = response.Entries?.filter(e => e.ErrorCode).map(e => ({
errorCode: e.ErrorCode,
errorMessage: e.ErrorMessage,
})) ?? [];
return {
content: [{
type: 'text',
text: JSON.stringify({
published: false,
failed_count: response.FailedEntryCount,
failures,
}),
}],
};
}
const eventIds = response.Entries?.map(e => e.EventId) ?? [];
return {
content: [{
type: 'text',
text: JSON.stringify({
published: true,
event_ids: eventIds,
count: eventIds.length,
}),
}],
};
}
);
Querying rules and targets
MCP server tools often need to describe the current routing configuration — which event patterns trigger which Lambda functions or Step Functions. Use ListRulesCommand to enumerate all rules on a bus, then ListTargetsByRuleCommand to resolve each rule's targets. Both APIs are paginated via NextToken. Rule patterns are JSON strings — parse them to display structured event filter conditions to an agent or user.
// MCP tool — list all rules on an event bus
server.tool(
'list_event_rules',
{
bus_name: z.string().optional(),
limit: z.number().int().min(1).max(100).default(50),
},
async ({ bus_name, limit }) => {
const busName = bus_name ?? EVENT_BUS_NAME;
const allRules: Array<Record<string, unknown>> = [];
let nextToken: string | undefined;
do {
const resp = await eb.send(new ListRulesCommand({
EventBusName: busName,
Limit: limit,
NextToken: nextToken,
}));
for (const rule of resp.Rules ?? []) {
allRules.push({
name: rule.Name,
state: rule.State, // 'ENABLED' | 'DISABLED'
description: rule.Description,
pattern: rule.EventPattern ? JSON.parse(rule.EventPattern) : null,
schedule: rule.ScheduleExpression ?? null,
arn: rule.Arn,
});
}
nextToken = resp.NextToken;
} while (nextToken);
return {
content: [{
type: 'text',
text: JSON.stringify({ bus: busName, rule_count: allRules.length, rules: allRules }, null, 2),
}],
};
}
);
// MCP tool — list targets for a specific rule
server.tool(
'list_rule_targets',
{
rule_name: z.string().min(1),
bus_name: z.string().optional(),
},
async ({ rule_name, bus_name }) => {
const resp = await eb.send(new ListTargetsByRuleCommand({
Rule: rule_name,
EventBusName: bus_name ?? EVENT_BUS_NAME,
}));
const targets = (resp.Targets ?? []).map(t => ({
id: t.Id,
arn: t.Arn, // Lambda ARN, SQS ARN, Step Functions ARN, etc.
role_arn: t.RoleArn,
input_transformer: t.InputTransformer
? { input_paths: t.InputTransformer.InputPathsMap, template: t.InputTransformer.InputTemplate }
: null,
}));
return {
content: [{
type: 'text',
text: JSON.stringify({ rule: rule_name, target_count: targets.length, targets }, null, 2),
}],
};
}
);
Schema Registry — discover and validate event schemas
The EventBridge Schema Registry discovers event schemas automatically from events flowing through your bus (when schema discovery is enabled) and stores them as OpenAPI 3 or JSONSchema Draft 4 documents. Your MCP server tools can query these schemas to validate event detail payloads before publishing, or to expose schema information to agents that need to understand the expected event structure.
import {
SchemasClient,
ListSchemasCommand,
DescribeSchemaCommand,
} from '@aws-sdk/client-schemas';
const schemasClient = new SchemasClient({ region: process.env.AWS_REGION ?? 'us-east-1' });
const REGISTRY_NAME = process.env.EVENTBRIDGE_REGISTRY ?? 'discovered-schemas';
// MCP tool — list known event schemas in the registry
server.tool(
'list_event_schemas',
{
registry: z.string().optional(),
name_prefix: z.string().optional(),
limit: z.number().int().min(1).max(100).default(20),
},
async ({ registry, name_prefix, limit }) => {
const resp = await schemasClient.send(new ListSchemasCommand({
RegistryName: registry ?? REGISTRY_NAME,
SchemaNamePrefix: name_prefix,
Limit: limit,
}));
return {
content: [{
type: 'text',
text: JSON.stringify({
registry: registry ?? REGISTRY_NAME,
schema_count: resp.Schemas?.length ?? 0,
schemas: (resp.Schemas ?? []).map(s => ({
name: s.SchemaName,
arn: s.SchemaArn,
version_count: s.VersionCount,
last_modified: s.LastModified,
})),
}, null, 2),
}],
};
}
);
// MCP tool — get the JSON Schema for a specific event type
server.tool(
'get_event_schema',
{
schema_name: z.string().min(1),
schema_version: z.string().optional(),
registry: z.string().optional(),
},
async ({ schema_name, schema_version, registry }) => {
const resp = await schemasClient.send(new DescribeSchemaCommand({
RegistryName: registry ?? REGISTRY_NAME,
SchemaName: schema_name,
SchemaVersion: schema_version,
}));
return {
content: [{
type: 'text',
text: JSON.stringify({
schema_name: resp.SchemaName,
schema_arn: resp.SchemaArn,
version: resp.SchemaVersion,
type: resp.Type,
last_modified: resp.LastModified,
content: resp.Content ? JSON.parse(resp.Content) : null,
}, null, 2),
}],
};
}
);
// Health probe — verify EventBridge bus is accessible
server.resource('eventbridge_health', 'event://aws/eventbridge/health', async () => {
try {
const resp = await eb.send(new DescribeEventBusCommand({ Name: EVENT_BUS_NAME }));
return {
contents: [{
uri: 'event://aws/eventbridge/health',
text: JSON.stringify({
status: 'ok',
bus_name: resp.Name,
bus_arn: resp.Arn,
}),
}],
};
} catch (err: any) {
return {
contents: [{
uri: 'event://aws/eventbridge/health',
text: JSON.stringify({ status: 'error', message: err.message }),
}],
};
}
});
EventBridge Schema Discovery must be explicitly enabled on each event bus (it is off by default). When enabled, EventBridge samples events flowing through the bus and creates or updates schemas in the discovery registry. Schemas from AWS service integrations (e.g., EC2 state change, S3 object created) are pre-populated in the aws.events registry and do not require discovery to be enabled.