AWS S3 · 2026-10-17 · S3 Advanced Features arc
AWS S3 Advanced Patterns for MCP Servers: Event-Driven Pipelines, Data Transformation, Multi-Tenant Isolation, and Cross-Region DR
Most MCP servers interact with S3 as a simple file store — put an object, get an object, done. The servers that operate reliably at scale go further: they use S3 Event Notifications to trigger tool workflows the moment a file lands, S3 Object Lambda to intercept and clean data before it reaches an LLM, S3 Access Points to enforce strict per-tenant namespace isolation without a sprawling bucket policy, and S3 Cross-Region Replication to ensure a DR region always has the data it needs to serve traffic the instant a Global Accelerator traffic dial switches over. This guide synthesises four S3 advanced feature areas into three structural patterns for MCP teams. Pattern 1 covers event-driven MCP tool pipelines — choosing among SQS, SNS, Lambda, and EventBridge notification destinations, configuring fan-out for multi-tool reactions, handling the at-least-once delivery guarantee with idempotent tool handlers, and monitoring the event pipeline for silent failures — drawing on S3 Event Notifications for MCP Servers. Pattern 2 covers data transformation and multi-tenant isolation — the S3 Object Lambda architecture for PII redaction and format conversion, the write-back pattern for large objects exceeding the 6 MB inline limit, tenant-specific Access Point policies that eliminate monolithic bucket policy sprawl, VPC-only access points for private MCP deployments, and how Object Lambda and Access Points combine for a complete per-tenant transformation pipeline — synthesised from S3 Object Lambda for MCP Servers and S3 Access Points for Multi-Tenant MCP Servers. Pattern 3 covers replication, failover, and replication-lag monitoring — CRR vs SRR for MCP data categories, Replication Time Control for SLA-bounded DR, bidirectional replication rules with loop prevention for active-active S3 architectures, Batch Replication for seeding existing objects to a new region, and AliveMCP probe strategy for detecting replication lag before a GA failover exposes a stale replica — drawn from S3 Replication for MCP Servers.
TL;DR
- Event-driven MCP tool pipelines: S3 supports four notification destinations — SQS (durable buffering, handles spiky upload volumes), SNS (fan-out to multiple queues or Lambda in a single notification), Lambda (low-latency, low-volume triggers), and EventBridge (richer filter logic by object size, storage class, ETag; unlimited targets per bucket; cross-account delivery). Use SQS for any MCP tool execution that takes more than 3 seconds — the visibility timeout acts as a concurrency limiter and prevents duplicate simultaneous executions of the same tool call. Use EventBridge for new architectures: one rule per bucket, unlimited patterns, and 18 additional event types vs the native notification system. Key trap: S3 event notifications encode object keys with URL percent-encoding — a file named
uploads/2026 Q4 report.pdfarrives asuploads/2026+Q4+report.pdfin the notification body; calldecodeURIComponent(key.replace(/\+/g, ' '))before using the key. Events are at-least-once — design every MCP tool handler to be idempotent by checking a processed-flag in your database before executing, keyed on thes3:object:eTagfrom the event body. Dead-letter queues with 14-day retention are mandatory; silent drops in a notification pipeline are invisible without them. AliveMCP monitors this by placing a synthetic canary object every 5 minutes into a watched prefix; if the corresponding SQS message does not appear in your queue within 120 seconds, it fires an alert — catching S3 permission errors, queue policy drift, and resource limits before a real upload goes unprocessed. - Data transformation and multi-tenant isolation: S3 Object Lambda places a Lambda function between a GetObject call and its caller — the stored object is unchanged; Lambda receives the raw bytes via presigned URL, transforms them, and calls
WriteGetObjectResponse. Three transformer patterns for MCP servers: (1) PII redaction — intercept all reads of raw customer documents and strip SSNs, emails, phone numbers, and credit card patterns before the bytes reach an LLM tool call; on transformation failure return a 500 to the caller rather than the raw object (never silently bypass redaction); (2) format conversion — translate CSV with inconsistent headers to normalised JSON that your MCP tool can parse without a bespoke ETL step per upload source; (3) tenant-specific filtering via IAM session tags — read the caller'saws:PrincipalTag/TenantIdfrom the Object Lambda event and strip rows that don't belong to that tenant. Hard limits: 60-second Lambda timeout, 6 MB maximum inline response viaWriteGetObjectResponse; for larger objects fetch the transformed result to a temporary S3 bucket and return a presigned URL in a 307 redirect instead of inline bytes. No caching — every GetObject call through an Object Lambda AP invokes Lambda; for objects read frequently without change, materialise the transformed copy as a permanent S3 object and use an ordinary S3 key rather than an Object Lambda AP. S3 Access Points address the bucket-policy sprawl problem: create one access point per tenant, scoped by policy todata/{tenant-id}/; each tenant IAM role receives access to only its access point; the bucket's delegation policy grants access to any same-account access point without enumerating tenants. Onboarding a new tenant = one CDK resource (access point + policy); offboarding = one resource delete; the shared bucket retains the data for retention-period archiving. VPC-only access points restrict access to calls originating from a specific VPC ID — use for private MCP deployments where S3 access should never traverse the public internet. Limit: 10,000 access points per bucket per region — supports up to 10,000 tenants per shared bucket before needing to shard buckets. - Replication, failover, and replication-lag monitoring: S3 Cross-Region Replication (CRR) copies objects to a bucket in a different region — required for any MCP architecture using Global Accelerator active-passive or active-active multi-region deployment, because the DR region's S3 bucket must already contain the data when the GA traffic dial switches. Without CRR, failover exposes an empty DR S3 bucket; every tool that reads from S3 fails immediately. CRR p99.9 completion: under 15 minutes without Replication Time Control; under 15 seconds with RTC (adds ~15% replication cost). Use RTC only for categories of S3 data whose absence during a GA failover directly causes tool failures — specifically the live configuration artifacts and user-uploaded files your MCP server reads on every tool call, not archival logs or analytics exports that degrade gracefully. Same-Region Replication (SRR) is for cross-account copies and compliance requiring two independent copies in the same geography; no inter-region data transfer cost. Bidirectional replication for active-active S3 requires two separate replication rules (A→B and B→A); enable ReplicaModifications: ENABLED on both rules — without this, objects replicated from A to B and then modified at B are not replicated back to A. Enable ReplicationTimeControl consistently on both directions to prevent A→B being slower than B→A, which causes write conflicts that last-writer-wins resolves silently. Selective replication by prefix+tag: apply CRR only to the
user-uploads/andconfig/prefixes (critical data needing RTC); excludelogs/andanalytics/(non-critical, cheaper without RTC). Batch Replication: enables existing objects to be copied to a new replication destination — run on day 1 when adding CRR to an existing bucket that already has objects, because standard CRR only applies to new puts after the replication rule is created. AliveMCP replication-lag probe: write a canary object to the source bucket every 5 minutes; check whether the replica bucket in the DR region contains a copy within the RTC SLA window (15s + 30s network margin); if the replication is late, alert before a real failover exposes a stale DR replica. Combine this with AliveMCP's main endpoint probe on the DR MCP server to catch cases where failover would succeed at the networking layer but fail at the S3-read layer. - Combining the four patterns end-to-end: A production MCP architecture for a multi-tenant document-analysis server: (1) tenants upload via pre-signed URLs to their access point (
ap-{tenant-id}) → access point policy enforces prefix isolation without touching the bucket policy; (2) S3 EventBridge rule fires on upload → routes to per-tenant SQS queues by matching the access point ARN in the event source field → MCP worker polls SQS and executes the analysis tool; (3) when the worker reads the uploaded document, it reads via the Object Lambda access point — PII redaction happens transparently before bytes reach the LLM; (4) CRR with RTC replicates alluser-uploads/objects to the DR region within 15 seconds, so that if GA fails over the DR MCP server can read the same data immediately. AliveMCP monitors the entire chain: access-point read probe (are tenant reads resolving through the correct access point?), event-delivery canary (are S3 events reaching SQS within 120s?), Object Lambda transform probe (does the transformation Lambda return 200 within 5s?), and replication-lag probe (does the DR bucket contain the latest canary object within 45s?). Any one of these silent failure modes would go undetected without active health monitoring — they don't produce 5xx errors at the MCP endpoint level; they surface only when a user's tool call returns empty results or corrupted data.
Pattern 1 — Event-Driven MCP Tool Pipelines with S3 Event Notifications
Why upload events are the natural entry point for MCP tool orchestration
Many MCP tool workflows are fundamentally file-triggered: a user uploads a PDF for summarisation, a CI pipeline deposits a test artifact for analysis, a batch ETL job writes a result file to a known prefix for ingestion. Polling S3 for new objects — scanning the bucket every N seconds — wastes Lambda invocations on a known-empty bucket between uploads, adds latency proportional to the polling interval, and misses objects uploaded in bursts that arrive faster than the poll cadence. S3 Event Notifications convert the upload moment into a push signal delivered to your processing infrastructure in under one second, with no polling.
The four notification destinations each make different tradeoffs that align with different MCP tool characteristics. Understanding the tradeoffs is the first design decision; the implementation details follow from it.
| Destination | Delivery latency | Filtering power | Fan-out | Best MCP use case |
|---|---|---|---|---|
| SQS (standard queue) | ~1s median, 15s p99 | Prefix + suffix only (S3 level) | One queue per bucket notification configuration | Durable work queue for slow tools (>3s); handles upload spikes via buffering |
| SNS | <1s | Prefix + suffix only | Fan-out to multiple SQS + Lambda subscriptions in one notification | One upload triggering multiple MCP tools (parse + index + notify) |
| Lambda | <1s (warm), 1–3s (cold) | Prefix + suffix only | One function per bucket notification configuration | Simple, fast reactions (<3s); avoid for high-upload-volume buckets |
| EventBridge | <1s | Object key, size, ETag, storage class, metadata | Multiple rules → multiple targets per bucket (unlimited) | New architectures; routing by file size or type; cross-account delivery |
SQS as a durable MCP tool work queue
The SQS pattern is the right default for any MCP tool that takes longer than 3 seconds to execute — document parsing, LLM inference, image analysis, code compilation. The queue acts as a shock absorber: if users upload 50 files in a burst, all 50 events queue immediately while your MCP worker processes them at the rate it can sustain. Without SQS, a Lambda burst would launch 50 concurrent function instances, each invoking a downstream LLM API, simultaneously exhausting your API rate limit quota and returning partial failures.
SQS visibility timeout is the critical sizing parameter. When a worker receives a message, SQS hides that message from other workers for the duration of the visibility timeout — if the worker dies or takes too long, the message reappears for retry. Set the visibility timeout to at least 6× your median tool execution time. If document parsing takes 45 seconds, set 300 seconds. The maxReceiveCount: 3 dead-letter queue configuration catches events that fail three consecutive times and routes them to the DLQ for inspection without loss.
import * as cdk from 'aws-cdk-lib';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as sqs from 'aws-cdk-lib/aws-sqs';
import * as s3n from 'aws-cdk-lib/aws-s3-notifications';
const dlq = new sqs.Queue(this, 'McpToolDlq', {
retentionPeriod: cdk.Duration.days(14),
encryption: sqs.QueueEncryption.SQS_MANAGED,
});
const processingQueue = new sqs.Queue(this, 'McpToolQueue', {
visibilityTimeout: cdk.Duration.seconds(300), // 6x median tool execution
retentionPeriod: cdk.Duration.days(4),
deadLetterQueue: { queue: dlq, maxReceiveCount: 3 },
encryption: sqs.QueueEncryption.SQS_MANAGED,
});
const uploadBucket = new s3.Bucket(this, 'McpUploadBucket', {
encryption: s3.BucketEncryption.S3_MANAGED,
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
versioned: true,
});
// Separate notification configurations per suffix — S3 can't combine them
uploadBucket.addEventNotification(
s3.EventType.OBJECT_CREATED,
new s3n.SqsDestination(processingQueue),
{ prefix: 'uploads/', suffix: '.pdf' },
);
uploadBucket.addEventNotification(
s3.EventType.OBJECT_CREATED,
new s3n.SqsDestination(processingQueue),
{ prefix: 'uploads/', suffix: '.docx' },
);
EventBridge for routing by file size, type, and cross-account fan-out
EventBridge is the right choice for new MCP event architectures because its filter language operates on the full event body — including object size, storage class, ETag, and user-defined metadata — rather than just the key prefix and suffix. This enables routing patterns impossible with native S3 notifications:
const bucket = new s3.Bucket(this, 'McpDataBucket', {
eventBridgeEnabled: true, // enables S3 → EventBridge delivery
encryption: s3.BucketEncryption.S3_MANAGED,
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
});
import * as events from 'aws-cdk-lib/aws-events';
import * as targets from 'aws-cdk-lib/aws-events-targets';
// Large files (>10MB) → slow-processing SQS queue
new events.Rule(this, 'LargeFileRule', {
eventPattern: {
source: ['aws.s3'],
detailType: ['Object Created'],
detail: {
bucket: { name: [bucket.bucketName] },
object: {
key: [{ prefix: 'uploads/' }],
size: [{ numeric: ['>=', 10_000_000] }],
},
},
},
targets: [new targets.SqsQueue(slowProcessingQueue)],
});
// Small files (<10MB) → fast-processing SQS queue
new events.Rule(this, 'SmallFileRule', {
eventPattern: {
source: ['aws.s3'],
detailType: ['Object Created'],
detail: {
bucket: { name: [bucket.bucketName] },
object: {
key: [{ prefix: 'uploads/' }],
size: [{ numeric: ['<', 10_000_000] }],
},
},
},
targets: [new targets.SqsQueue(fastProcessingQueue)],
});
EventBridge also supports cross-account event buses — you can route S3 events from your data-lake account directly to MCP tool queues in separate application accounts without intermediate Lambda glue functions. For an enterprise MCP architecture where each business unit has its own AWS account but shares a central data bucket, EventBridge is the only native way to deliver upload events cross-account without custom relay code.
The key-encoding trap that silently breaks MCP tool lookups
S3 event notifications encode object keys using URL percent-encoding with a twist: spaces are encoded as +, not %20. A file uploaded as uploads/Q4 2026 Report.pdf arrives in the SQS message body as uploads/Q4+2026+Report.pdf. If your MCP worker passes that raw string directly to a GetObjectCommand, S3 returns a 404 — the plus signs are not valid filename characters in the stored key.
The fix is one line, but it must be applied consistently at every point where your code reads a key from an S3 event notification body:
// In your SQS consumer, before any S3 API call:
const rawKey = record.body.s3.object.key; // from SQS message
const key = decodeURIComponent(rawKey.replace(/\+/g, ' '));
// key is now the actual S3 object key, safe to pass to GetObjectCommand
This trap appears in CloudWatch logs as NoSuchKey errors on objects you can see in the S3 console — because the console displays the decoded key. AliveMCP's event pipeline canary catches this class of failure: the canary object key always contains a space character, so if the notification handler has the encoding bug, the first canary cycle reveals it before a real user upload is dropped.
Idempotency: at-least-once delivery with duplicate-safe tool handlers
S3 event notifications guarantee at-least-once delivery. Duplicate events are rare but real — they occur during S3 infrastructure maintenance events, when a notification delivery attempt times out and S3 retries, or when a worker crashes after processing an event but before deleting the SQS message. Your MCP tool handler must be idempotent: executing it twice on the same input must produce the same result as executing it once.
The idempotency key is the combination of the S3 bucket name, object key, and ETag from the event body. The ETag is an MD5 hash of the object content — it changes if the object is overwritten, so checking the ETag prevents re-processing a new version of a file that replaced an old one at the same key.
// In your MCP tool handler:
async function processS3Upload(event: S3EventRecord): Promise<void> {
const { bucket, object } = event.s3;
const key = decodeURIComponent(object.key.replace(/\+/g, ' '));
const etag = object.eTag; // MD5 of object content
const idempotencyKey = `${bucket.name}/${key}/${etag}`;
// Check if already processed (using SQLite or DynamoDB)
const existing = db.prepare(
'SELECT id FROM processed_uploads WHERE idempotency_key = ?'
).get(idempotencyKey);
if (existing) {
console.log(`Skipping duplicate event for ${idempotencyKey}`);
return; // delete the SQS message — we already processed this object
}
// Execute the MCP tool
const result = await executeMcpTool({ bucket: bucket.name, key, etag });
// Record as processed — do this AFTER the tool succeeds
db.prepare(
'INSERT INTO processed_uploads (idempotency_key, processed_at) VALUES (?, ?)'
).run(idempotencyKey, new Date().toISOString());
}
Record the idempotency key after the tool succeeds, not before. If you record it first and the tool fails, the event is marked as processed but the work was not done — you've silently dropped it without a retry. The correct order is: check → execute → record → delete SQS message.
For detailed configuration of all four notification destination types, filter patterns, and fan-out architectures, see S3 Event Notifications for MCP Servers.
Pattern 2 — Data Transformation and Multi-Tenant Isolation: Object Lambda and Access Points
The problem: every tool reads raw data
A multi-tenant MCP document server has a default data flow problem: raw customer documents stored in S3 often contain PII — SSNs, emails, phone numbers — that should not reach an LLM tool call. The obvious fix is to add redaction logic to each MCP tool individually. But as the number of tools grows (summarise, extract-entities, classify, index), each tool independently re-implements redaction logic, each implementation diverges slightly, and a regex update must be deployed across N tools simultaneously. One tool that forgets the update passes raw PII to the LLM.
S3 Object Lambda solves this at the infrastructure layer: add redaction once, at the S3 read path, and every tool that reads through the Object Lambda access point gets the cleaned version automatically — whether it's a new tool added next month or an old tool that has never been updated.
The Object Lambda architecture
Object Lambda uses three resources: a standard S3 access point (the backing access point), an Object Lambda access point, and a Lambda function. The flow is:
- MCP tool calls
GetObjectusing the Object Lambda access point ARN as the bucket parameter - S3 intercepts the request and invokes the Lambda with: the original request parameters, a presigned URL to fetch the raw object from the backing access point, and caller identity context (IAM principal, session tags)
- Lambda fetches the raw object via presigned URL, transforms it, and calls
WriteGetObjectResponsewith the transformed bytes - S3 delivers the transformed bytes to the MCP tool caller
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as s3objectlambda from 'aws-cdk-lib/aws-s3objectlambda';
import * as lambda from 'aws-cdk-lib/aws-lambda';
const dataBucket = new s3.Bucket(this, 'DataBucket', {
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
encryption: s3.BucketEncryption.S3_MANAGED,
versioned: true,
});
// Standard access point — the backing access point for Object Lambda
const backingAP = new s3.CfnAccessPoint(this, 'BackingAP', {
bucket: dataBucket.bucketName,
name: 'mcp-data-backing',
});
// Transformation Lambda
const transformFn = new lambda.Function(this, 'PiiRedactFn', {
runtime: lambda.Runtime.NODEJS_22_X,
handler: 'index.handler',
code: lambda.Code.fromAsset('lambda/pii-redact'),
timeout: cdk.Duration.seconds(60),
memorySize: 1024,
});
transformFn.addToRolePolicy(new iam.PolicyStatement({
actions: ['s3-object-lambda:WriteGetObjectResponse'],
resources: ['*'],
}));
// Object Lambda access point — the endpoint MCP tools use
const objectLambdaAP = new s3objectlambda.CfnAccessPoint(this, 'PiiRedactAP', {
name: 'mcp-data-pii-clean',
objectLambdaConfiguration: {
supportingAccessPoint: backingAP.attrArn,
transformationConfigurations: [{
actions: ['GetObject'],
contentTransformation: {
awsLambda: { functionArn: transformFn.functionArn },
},
}],
},
});
PII redaction: the failure-mode contract
The most important implementation detail for a PII redaction transformer is the failure-mode contract: if the redaction Lambda fails for any reason, it must return a 500 error to the caller — not the raw object. Returning the raw object on error would silently bypass redaction, which is worse than a failed tool call from a compliance perspective.
// lambda/pii-redact/index.ts
const PII_PATTERNS: Array<[RegExp, string]> = [
[/\b\d{3}-\d{2}-\d{4}\b/g, '[SSN REDACTED]'],
[/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g, '[EMAIL REDACTED]'],
[/\b\d{3}[-.\s]?\d{3}[-.\s]?\d{4}\b/g, '[PHONE REDACTED]'],
[/\b(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|3[47][0-9]{13})\b/g, '[CARD REDACTED]'],
];
export async function handler(event: any): Promise<void> {
const { outputRoute, outputToken, inputS3Url } = event.getObjectContext;
try {
const rawBytes = await fetchUrl(inputS3Url);
let text = rawBytes.toString('utf-8');
for (const [pattern, replacement] of PII_PATTERNS) {
text = text.replace(pattern, replacement);
}
const transformed = Buffer.from(text, 'utf-8');
await s3.send(new WriteGetObjectResponseCommand({
RequestRoute: outputRoute,
RequestToken: outputToken,
Body: transformed,
ContentLength: transformed.length,
ContentType: 'text/plain',
StatusCode: 200,
}));
} catch (err) {
// CRITICAL: return 500, not the raw object
await s3.send(new WriteGetObjectResponseCommand({
RequestRoute: outputRoute,
RequestToken: outputToken,
StatusCode: 500,
ErrorCode: 'TransformationFailed',
ErrorMessage: `PII redaction failed: ${(err as Error).message}`,
}));
}
}
The 6 MB limit and the write-back pattern for large documents
Object Lambda's WriteGetObjectResponse accepts a maximum of 6 MB inline in the response body. A multi-page PDF or a large CSV file may exceed this limit. The write-back pattern handles this: instead of returning the transformed bytes inline, Lambda writes the transformed result to a temporary S3 prefix and returns a 307 redirect to a presigned URL for that result. The caller follows the redirect and fetches the large transformed object directly from S3.
// Write-back pattern for large objects
const transformedKey = `transformed/${outputToken}.txt`;
await s3source.send(new PutObjectCommand({
Bucket: process.env.TRANSFORM_BUCKET,
Key: transformedKey,
Body: largeTransformedBytes,
ContentType: 'text/plain',
}));
const presignedUrl = await getSignedUrl(s3source, new GetObjectCommand({
Bucket: process.env.TRANSFORM_BUCKET,
Key: transformedKey,
}), { expiresIn: 300 });
// Return redirect — S3 Object Lambda supports 307 for large objects
await s3.send(new WriteGetObjectResponseCommand({
RequestRoute: outputRoute,
RequestToken: outputToken,
StatusCode: 307,
Redirect: presignedUrl,
}));
S3 Access Points: per-tenant namespace isolation without bucket policy sprawl
The classic multi-tenant S3 problem: you store all tenant data in one shared bucket (shared lifecycle, encryption, replication — simpler operations) but each tenant's IAM role must only access its own prefix. The naive solution — one bucket policy enumerating every tenant's principal and prefix — works for 5 tenants, breaks for 50, and becomes unreviewed and dangerous at 500.
S3 Access Points solve this by delegating per-tenant access control to separate, independently-managed access point policies. Create one access point per tenant, scoped to data/{tenant-id}/. Each tenant IAM role gets access to only its access point. The shared bucket policy delegates to any access point in the same account with a single line — no per-tenant changes to the bucket policy when onboarding or offboarding.
| Operation | Single bucket policy | One bucket per tenant | S3 Access Points |
|---|---|---|---|
| Onboard new tenant | Edit bucket policy (full redeploy) | Create bucket + policy + lifecycle (3 resources) | Create access point + policy (2 resources, no bucket policy change) |
| Offboard tenant | Edit bucket policy (full redeploy) | Delete bucket (drain data first) | Delete access point (data stays for retention archiving) |
| Policy blast radius | One malformed policy breaks all tenants | Isolated per bucket | Isolated per access point — one broken AP affects only one tenant |
| Operational overhead | One policy grows without bound | 10,000 bucket limit per account | 10,000 access points per bucket — 10,000 tenants per shared bucket |
// Shared bucket with delegation policy
const dataBucket = new s3.Bucket(this, 'TenantDataBucket', {
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
encryption: s3.BucketEncryption.S3_MANAGED,
versioned: true,
});
// Bucket policy delegates to all access points in this account
dataBucket.addToResourcePolicy(new iam.PolicyStatement({
sid: 'DelegateToAccessPoints',
principals: [new iam.AnyPrincipal()],
actions: ['s3:*'],
resources: [dataBucket.bucketArn, `${dataBucket.bucketArn}/*`],
conditions: {
StringEquals: { 's3:DataAccessPointAccount': this.account },
},
}));
// Per-tenant access point — repeat for each tenant
function createTenantAccessPoint(tenantId: string, tenantRoleArn: string) {
return new s3.CfnAccessPoint(this, `AP-${tenantId}`, {
bucket: dataBucket.bucketName,
name: `mcp-tenant-${tenantId}`,
policy: {
Version: '2012-10-17',
Statement: [{
Effect: 'Allow',
Principal: { AWS: tenantRoleArn },
Action: ['s3:GetObject', 's3:PutObject', 's3:DeleteObject'],
Resource: `arn:aws:s3:*:*:accesspoint/mcp-tenant-${tenantId}/object/data/${tenantId}/*`,
}],
},
});
}
VPC-only access points for private MCP deployments
For MCP servers deployed inside a VPC where S3 access should never traverse the public internet, access points support a VPC restriction that blocks all calls not originating from the specified VPC — even calls authenticated with valid IAM credentials from outside the VPC are rejected.
new s3.CfnAccessPoint(this, 'PrivateMcpAP', {
bucket: dataBucket.bucketName,
name: 'mcp-private-internal',
vpcConfiguration: {
vpcId: vpc.vpcId, // Only calls from this VPC are accepted
},
policy: {
Version: '2012-10-17',
Statement: [{
Effect: 'Allow',
Principal: { AWS: mcpServerRole.roleArn },
Action: ['s3:GetObject', 's3:PutObject'],
Resource: 'arn:aws:s3:*:*:accesspoint/mcp-private-internal/object/*',
Condition: {
StringEquals: { 'aws:SourceVpc': vpc.vpcId },
},
}],
},
});
Combining Object Lambda and Access Points for a per-tenant transformation pipeline
Object Lambda and Access Points compose: create one Object Lambda access point per tenant, backed by that tenant's standard access point (which is already scoped to their prefix). Each tenant's MCP tool calls GetObject through its Object Lambda AP; the Lambda function reads the caller's aws:PrincipalTag/TenantId from the event context and can apply tenant-specific transformation rules — different PII patterns for different regulatory environments, different field masks for different data classification levels.
The architecture: Tenant IAM role → Object Lambda AP (per-tenant) → Backing AP (per-tenant, scoped to data/{tenant-id}/) → Lambda transformer (reads TenantId tag) → shared bucket. Each tenant sees only its own data, transformed according to its own rules, through its own independently-managed policy chain.
For full implementation details and the format conversion (CSV → normalised JSON) and write-back patterns, see S3 Object Lambda for MCP Servers and S3 Access Points for Multi-Tenant MCP Servers.
Pattern 3 — Replication, Failover Integration, and Replication-Lag Monitoring
Why S3 replication is a prerequisite for GA failover, not an optional add-on
Teams deploying MCP servers behind AWS Global Accelerator active-passive or active-active multi-region configurations often configure the GA traffic dial and the multi-region ECS deployment first, leaving "S3 replication" as a follow-up item. This is backwards. An MCP server that fails over to a DR region needs the data in that region's S3 bucket the moment the first tool call arrives. Without replication, the bucket is empty; every tool that reads from S3 returns 404 or empty results; users experience a degraded failover that is operationally worse than the original outage.
S3 replication is a prerequisite for GA-based DR, not a follow-up. The right sequence: configure CRR → verify replication is healthy → then enable GA multi-region routing. Replication takes minutes to catch up on existing objects (via Batch Replication) and then operates continuously for new objects.
CRR vs SRR: choosing for each category of MCP data
Not all S3 data needs the same replication treatment. Map your MCP server's S3 objects by their DR impact:
| S3 Prefix / Data Category | DR Impact if Missing at Failover | Recommended Replication | Use RTC? |
|---|---|---|---|
user-uploads/ — customer-uploaded documents for tool processing |
Critical — tool calls return 404; users see failures | CRR to DR region | Yes — 15s SLA |
config/ — MCP server configuration artifacts, tool schemas |
Critical — server starts but tools may behave incorrectly without latest config | CRR to DR region | Yes — 15s SLA |
model-outputs/ — LLM responses cached for de-duplication |
Moderate — cache misses cause re-inference, higher cost, higher latency | CRR to DR region | No — 15min p99.9 acceptable |
logs/ — access logs, tool execution traces |
None — logs are write-once audit trails, not read on the data path | SRR to compliance account | No |
analytics/ — aggregated usage reports, billing snapshots |
None — analytics reads degrade gracefully to cached/stale data | None or SRR | No |
Configuring CRR with RTC for critical prefixes
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as iam from 'aws-cdk-lib/aws-iam';
const sourceBucket = new s3.Bucket(this, 'McpSourceBucket', {
bucketName: 'mcp-data-us-east-1',
versioned: true, // Required for replication
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
encryption: s3.BucketEncryption.S3_MANAGED,
});
const replicationRole = new iam.Role(this, 'ReplicationRole', {
assumedBy: new iam.ServicePrincipal('s3.amazonaws.com'),
inlinePolicies: {
ReplicationPolicy: new iam.PolicyDocument({
statements: [
new iam.PolicyStatement({
actions: ['s3:GetReplicationConfiguration', 's3:ListBucket'],
resources: [sourceBucket.bucketArn],
}),
new iam.PolicyStatement({
actions: ['s3:GetObjectVersionForReplication', 's3:GetObjectVersionAcl', 's3:GetObjectVersionTagging'],
resources: [`${sourceBucket.bucketArn}/*`],
}),
new iam.PolicyStatement({
actions: ['s3:ReplicateObject', 's3:ReplicateDelete', 's3:ReplicateTags'],
resources: [`${destinationBucketArn}/*`],
}),
],
}),
},
});
// Apply replication configuration with RTC on critical prefixes only
const cfnSourceBucket = sourceBucket.node.defaultChild as s3.CfnBucket;
cfnSourceBucket.replicationConfiguration = {
role: replicationRole.roleArn,
rules: [
{
id: 'CriticalDataRTC',
status: 'Enabled',
filter: {
// Only replicate user-uploads/ and config/ with RTC
prefix: 'user-uploads/',
},
destination: {
bucket: destinationBucketArn,
replicationTime: {
status: 'Enabled',
time: { minutes: 15 }, // RTC: replicate within 15 minutes at p99.9
},
metrics: {
status: 'Enabled',
eventThreshold: { minutes: 15 },
},
},
deleteMarkerReplication: { status: 'Enabled' },
},
{
id: 'ConfigRTC',
status: 'Enabled',
filter: { prefix: 'config/' },
destination: {
bucket: destinationBucketArn,
replicationTime: { status: 'Enabled', time: { minutes: 15 } },
metrics: { status: 'Enabled', eventThreshold: { minutes: 15 } },
},
deleteMarkerReplication: { status: 'Enabled' },
},
{
id: 'ModelOutputsNoRTC',
status: 'Enabled',
filter: { prefix: 'model-outputs/' },
destination: {
bucket: destinationBucketArn,
// No RTC for model outputs — 15-minute p99.9 is acceptable
},
},
],
};
Bidirectional replication for active-active S3: loop prevention
Active-active MCP deployments — both regions serving live traffic simultaneously — require bidirectional S3 replication. Region A's MCP servers write user uploads to A's bucket; Region B's MCP servers write to B's bucket; both buckets need to see both sets of objects eventually.
Bidirectional replication requires two separate CRR rules: one from A→B and one from B→A. Without careful configuration, you create a replication loop: an object written to A is replicated to B, which triggers B→A replication, replicating it back to A, which triggers A→B again, infinitely. S3 prevents this loop automatically for exact replica objects — an object replicated from A to B is flagged as a replica and excluded from B→A rules. But two conditions can break loop prevention:
- ReplicaModifications not enabled: if a replica object at B is modified after replication (metadata update, content overwrite), S3 treats it as a new object, not a replica — enable
ReplicaModifications: ENABLEDon both rules to replicate these modifications back to A without triggering a loop - SSE-KMS with mismatched keys: if A uses KMS key A and B uses KMS key B, an object replicated from A to B is re-encrypted with B's key — S3 may not recognise it as the same object for loop prevention purposes; use the same CMK in both regions or use SSE-S3 for cross-region replicated objects
// Rule at bucket-A: replicate to B, include modifications to replicas
{
id: 'AToBBidirectional',
status: 'Enabled',
sourceSelectionCriteria: {
replicaModifications: { status: 'Enabled' }, // replicate modifications to replicas
},
destination: { bucket: bucketBArn, ... },
}
// Rule at bucket-B: replicate to A (symmetric)
{
id: 'BToABidirectional',
status: 'Enabled',
sourceSelectionCriteria: {
replicaModifications: { status: 'Enabled' },
},
destination: { bucket: bucketAArn, ... },
}
Batch Replication: seeding the DR bucket for existing objects
Standard CRR only applies to objects written after the replication rule is created. If your source bucket already has 50 TB of user uploads when you add CRR, those existing objects are not replicated automatically. Run S3 Batch Replication as a one-time job on day 1 to copy all existing objects to the DR bucket, then let ongoing CRR handle new objects.
# Create an S3 Batch Operations job to replicate existing objects
aws s3control create-job \
--account-id 123456789 \
--operation '{"S3ReplicateObject": {}}' \
--report '{"Bucket": "arn:aws:s3:::mcp-batch-reports", "Format": "Report_CSV_20180820", "Enabled": true, "ReportScope": "AllTasks"}' \
--manifest-generator '{
"S3JobManifestGenerator": {
"SourceBucket": "arn:aws:s3:::mcp-data-us-east-1",
"EnableManifestOutput": false,
"Filter": {
"ObjectReplicationStatuses": ["NONE", "FAILED"]
}
}
}' \
--role-arn arn:aws:iam::123456789:role/BatchReplicationRole \
--priority 10 \
--no-confirmation-required
The filter ObjectReplicationStatuses: ["NONE", "FAILED"] catches two categories: objects that existed before CRR was enabled (NONE) and objects where a previous replication attempt failed (FAILED). Run this job on a schedule (e.g., weekly) to catch any replication failures that accumulated since the last run.
AliveMCP replication-lag monitoring: catching stale DR before failover
Replication monitoring is fundamentally a lag problem: you can't know replication is failing until you measure whether replicated objects are arriving within the expected SLA window. The standard S3 CloudWatch metric ReplicationLatency shows the time between object creation and replication completion in minutes — but it only covers successfully replicated objects, not objects stuck in the replication queue due to permission errors, KMS key mismatches, or destination bucket policy issues.
AliveMCP's replication-lag probe works differently: it writes a synthetic canary object with a known timestamp to the source bucket every 5 minutes, then checks whether that specific object key exists in the DR replica bucket within the RTC SLA window (15 seconds + 30 seconds network margin = 45 seconds total). If the canary object is not present in the DR bucket after 45 seconds, it fires an alert with the specific source key and the observed lag — distinguishing between RTC SLA violation (object exists but arrived late), replication failure (object never arrived), and permission error (403 from the DR bucket on the check call).
// Replication lag probe (runs every 5 minutes in AliveMCP worker)
async function probeReplicationLag(sourceBucket: string, drBucket: string, drRegion: string) {
const canaryKey = `canary/replication/${Date.now()}.json`;
const timestamp = new Date().toISOString();
// Write canary to source bucket
await s3Source.send(new PutObjectCommand({
Bucket: sourceBucket,
Key: canaryKey,
Body: JSON.stringify({ timestamp, probe: 'replication-lag' }),
}));
// Poll the DR bucket for up to 60 seconds
const startMs = Date.now();
const slaMs = 45_000; // 15s RTC + 30s margin
while (Date.now() - startMs < slaMs + 15_000) {
await sleep(5_000);
try {
await s3Dr.send(new HeadObjectCommand({ Bucket: drBucket, Key: canaryKey }));
const lagMs = Date.now() - startMs;
recordMetric('replication_lag_ms', lagMs);
if (lagMs > slaMs) {
fireAlert(`Replication RTC SLA exceeded: ${lagMs}ms (SLA ${slaMs}ms) for key ${canaryKey}`);
}
return; // Replication succeeded
} catch (err) {
if ((err as any).name !== 'NotFound') throw err;
}
}
// Object not found after timeout
fireAlert(`Replication FAILURE: canary object not replicated to DR after ${Date.now() - startMs}ms: ${canaryKey}`);
}
Pair the replication-lag probe with AliveMCP's main endpoint probe on the DR MCP server itself. The endpoint probe checks whether the DR MCP server responds to tool calls; the replication-lag probe checks whether the data those tool calls depend on is present. Both must be green before you trust a GA traffic dial switch to the DR region — a green endpoint probe with a red replication-lag probe means the DR server is up but would return empty results for any tool that reads user data from S3.
For CRR vs SRR comparison tables, Replication Time Control configuration, and selective replication by prefix and tag, see S3 Replication for MCP Servers.
Putting It Together: A Complete Multi-Tenant Document Processing Architecture
The four S3 advanced features compose naturally into a production multi-tenant MCP document analysis server. Here is how the four patterns connect:
Upload path: Access Points + Event Notifications
- A tenant uploads a document via a presigned URL generated against their tenant access point (
ap-{tenant-id}). The access point policy enforces that the object lands inuser-uploads/{tenant-id}/— the tenant cannot write to any other prefix, and no other tenant can read their prefix, regardless of bucket policy state. - S3 EventBridge fires an Object Created event. An EventBridge rule matches events from this bucket and routes them to a per-tenant SQS queue, identified by matching the access point ARN in the event detail. The MCP worker polling that queue processes only that tenant's uploads.
- The SQS consumer decodes the URL-encoded object key, checks the idempotency table by ETag, and if not already processed, invokes the document analysis MCP tool.
Read path: Object Lambda + Access Points
- The MCP tool reads the uploaded document using the Object Lambda access point (
olap-{tenant-id}), which is backed by the tenant's standard access point. - The PII redaction Lambda intercepts the read, fetches the raw document from the backing access point via presigned URL, applies redaction patterns (with tenant-specific patterns loaded from a config table keyed by TenantId session tag), and returns the cleaned bytes via
WriteGetObjectResponse. - The MCP tool sees only the redacted document. The raw version remains in S3 for compliance archiving, accessible only through direct API calls (not through the Object Lambda AP) with a separate IAM policy requiring MFA.
DR path: CRR + Global Accelerator
- CRR with RTC replicates every new object under
user-uploads/andconfig/to the DR bucket in us-west-2 within 15 seconds. - The DR region has its own Access Points and Object Lambda APs with identical configuration, deployed via CDK with the region parameter overridden.
- AliveMCP's replication-lag probe verifies every 5 minutes that the DR bucket's
user-uploads/prefix contains the latest canary object within 45 seconds. AliveMCP's endpoint probe verifies the DR MCP server responds to tool calls. - If the primary region fails, the GA traffic dial switches to the DR region. The DR MCP server immediately serves tool calls against the DR S3 bucket — which already has all user uploads from the last 15 seconds of replication. Tenants see at most 15 seconds of missing upload data — only the objects that were in-flight during the replication window.
Operational monitoring: what AliveMCP watches end-to-end
| Probe | What it detects | Alert threshold |
|---|---|---|
| Event delivery canary (S3 → SQS) | S3 permission drift, SQS policy misconfiguration, resource limit exhaustion preventing notification delivery | Canary object's SQS message not received within 120s |
| Object Lambda transform probe | PII redaction Lambda timeout, runtime errors, WriteGetObjectResponse failures, Object Lambda AP misconfiguration |
Transform call fails or returns non-200 within 5s |
| Access point read probe | Access point policy drift, bucket delegation policy removed, IAM role change blocking tenant access | GetObject via access point returns non-200 within 3s |
| Replication lag probe (primary → DR) | CRR rule disabled, KMS key permission revoked, DR bucket policy change blocking replication, RTC SLA exceeded | Canary object not present in DR bucket within 45s |
| DR endpoint probe | DR ECS service unhealthy, GA endpoint group pointing to unhealthy NLB, DR MCP server crash | MCP server at DR endpoint returns non-200 within 5s |
The last row is the key insight: the GA endpoint probe and the replication-lag probe are independent failure modes. An MCP server that is up (endpoint probe green) but has a stale S3 replica (replication probe red) would pass a conventional uptime check and fail in production the moment a tool tries to read user data. Both probes are required for a reliable DR posture — one verifies the network path, the other verifies the data plane.
Further Reading
- S3 Event Notifications for MCP Servers — full coverage of all four notification destinations, EventBridge content filtering, DLQ configuration, and fan-out patterns
- S3 Object Lambda for MCP Servers — complete Object Lambda architecture, format conversion (CSV → JSON), tenant-specific filtering via IAM session tags, write-back pattern for large objects
- S3 Access Points for Multi-Tenant MCP Servers — per-tenant access point CDK code, VPC-only access points, cross-account access patterns, CloudTrail audit integration
- S3 Replication for MCP Servers — CRR vs SRR comparison table, Replication Time Control configuration, bidirectional replication loop prevention, Batch Replication for existing objects
- MCP Tools for AWS S3 — S3 fundamentals: GetObject, PutObject, multipart upload, lifecycle rules, and presigned URLs for MCP servers
- AWS Global Accelerator for MCP Servers — multi-region deployment, traffic dials, health checks, and active-passive vs active-active architecture that CRR enables