Guide · AWS PrivateLink · Cross-Account Service Access

AWS PrivateLink for MCP Servers — Expose as VPC Endpoint Service and Enable Cross-Account Access Without Peering

AWS PrivateLink lets you expose an MCP server running behind a Network Load Balancer as a named VPC Endpoint Service, so consumers in other VPCs — even in different AWS accounts — can connect to it via Interface endpoints without VPC peering, without routing CIDRs, and without touching the public internet. This is the right architecture when your organization runs MCP servers in a central shared-services account and needs to give access to multiple product-team accounts; when you're building an MCP server product and want enterprise customers to connect directly to your endpoint from within their own VPCs; or when consumer VPCs have overlapping CIDR blocks that make peering impossible. The fundamental difference from VPC peering: PrivateLink is unidirectional and service-scoped — the consumer can reach the specific service endpoint and nothing else in the provider VPC. No route tables are merged, no CIDR conflicts, no lateral movement risk.

TL;DR

Provider side: put the MCP server behind an NLB, then create a VPC Endpoint Service pointing at the NLB. Consumer side: create an Interface VPC endpoint pointing at the service name (format: com.amazonaws.vpce.us-east-1.vpce-svc-xxx). The consumer gets a private DNS name inside their VPC. For branded DNS, the provider can associate a private hosted zone so consumers get mcp.yourcompany.com instead of the vpce-xxx name. Connection acceptance can be auto-accept (for intra-org use) or manual (for external consumer access control).

Provider setup — NLB and VPC Endpoint Service

The MCP server must be behind a Network Load Balancer. Application Load Balancers are not supported as PrivateLink targets — you must use NLB. The NLB can be in the same VPC as the MCP server and does not need to be internet-facing. Create the endpoint service pointing at the NLB ARN; AWS then assigns a service name and manages the cross-VPC connectivity.

# 1. Create an NLB targeting the MCP server (if not already behind one)
NLB_ARN=$(aws elbv2 create-load-balancer \
  --name mcp-server-internal \
  --type network \
  --scheme internal \
  --subnets subnet-0private-a subnet-0private-b \
  --query 'LoadBalancers[0].LoadBalancerArn' --output text)

# 2. Create a target group for the MCP server (HTTP on port 3000)
TG_ARN=$(aws elbv2 create-target-group \
  --name mcp-server-tg \
  --protocol TCP \
  --port 3000 \
  --vpc-id vpc-0abc123 \
  --target-type ip \
  --health-check-protocol TCP \
  --health-check-port 3000 \
  --query 'TargetGroups[0].TargetGroupArn' --output text)

# 3. Create NLB listener on port 443 forwarding to target group
aws elbv2 create-listener \
  --load-balancer-arn $NLB_ARN \
  --protocol TLS \
  --port 443 \
  --certificates CertificateArn=arn:aws:acm:us-east-1:123456789012:certificate/abc \
  --default-actions Type=forward,TargetGroupArn=$TG_ARN

# 4. Create the VPC Endpoint Service pointing at the NLB
SVC_ID=$(aws ec2 create-vpc-endpoint-service-configuration \
  --network-load-balancer-arns $NLB_ARN \
  --acceptance-required \
  --query 'ServiceConfiguration.ServiceId' --output text)

# Get the service name that consumers will use
aws ec2 describe-vpc-endpoint-service-configurations \
  --service-ids $SVC_ID \
  --query 'ServiceConfigurations[0].ServiceName'
# Output: com.amazonaws.vpce.us-east-1.vpce-svc-0abc123

Set --acceptance-required if you want to manually approve each consumer connection request — appropriate for external customers. For internal organizational use across accounts in an AWS Organization, you can set --no-acceptance-required (auto-accept) and restrict which AWS principal ARNs are allowed to create endpoints using the modify-vpc-endpoint-service-permissions command. NLB health checks to the MCP server must pass for consumers to connect — an unhealthy target group takes the endpoint offline from the consumer's perspective.

Consumer setup — Interface endpoint creation

The consumer creates an Interface VPC endpoint specifying the provider's service name. AWS routes the endpoint's traffic through the AWS backbone to the provider's NLB. The consumer gets an endpoint-specific DNS name and, with private DNS correctly configured on the provider side, can optionally get a custom domain that resolves within their VPC.

# Consumer account: create an Interface endpoint to the provider's service
aws ec2 create-vpc-endpoint \
  --vpc-endpoint-type Interface \
  --service-name com.amazonaws.vpce.us-east-1.vpce-svc-0abc123 \
  --vpc-id vpc-0consumer \
  --subnet-ids subnet-0consumer-a subnet-0consumer-b \
  --security-group-ids sg-0consumer-mcp \
  --region us-east-1

# Get the endpoint DNS names
aws ec2 describe-vpc-endpoints \
  --filters "Name=service-name,Values=com.amazonaws.vpce.us-east-1.vpce-svc-0abc123" \
  --query 'VpcEndpoints[0].DnsEntries'
# Returns:
# - vpce-0xyz.vpce-svc-0abc123.us-east-1.vpce.amazonaws.com (endpoint-specific)
# - vpce-0xyz-us-east-1a.vpce-svc-0abc123.us-east-1.vpce.amazonaws.com (AZ-specific)

# Test connectivity from consumer VPC
curl https://vpce-0xyz.vpce-svc-0abc123.us-east-1.vpce.amazonaws.com/health
# or using custom DNS if provider has set up private hosted zone:
curl https://mcp.provider-company.com/health

The consumer's security group on the endpoint (sg-0consumer-mcp) controls what the endpoint allows — think of it as the "what can reach the endpoint ENI" layer. The consumer also needs an outbound rule from the application security group to the endpoint's security group. The NLB on the provider side does not expose source IPs to the MCP server — the source IP in the MCP server will be the NLB node's private IP, not the consumer's actual IP. If source IP attribution matters, use a Proxy Protocol v2 listener policy on the NLB.

Connection acceptance and allowlist management

When acceptance-required is true, every consumer endpoint creation request enters a "pendingAcceptance" state and must be explicitly approved. This is the right default for external-facing services — a consumer cannot connect until you approve their AWS account. For organizational use, use permissions to restrict who can create endpoints, and disable acceptance to reduce operational overhead.

# List pending connection requests
aws ec2 describe-vpc-endpoint-connections \
  --filters "Name=vpc-endpoint-service-id,Values=$SVC_ID" \
            "Name=vpc-endpoint-state,Values=pendingAcceptance" \
  --query 'VpcEndpointConnections[*].{ID:VpcEndpointId,Account:VpcEndpointOwner,State:VpcEndpointState}'

# Accept specific endpoint connections
aws ec2 accept-vpc-endpoint-connections \
  --service-id $SVC_ID \
  --vpc-endpoint-ids vpce-0consumer1 vpce-0consumer2

# Reject a connection request
aws ec2 reject-vpc-endpoint-connections \
  --service-id $SVC_ID \
  --vpc-endpoint-ids vpce-0unwanted

# Grant permission to specific AWS principals to create endpoints (allowlist)
aws ec2 modify-vpc-endpoint-service-permissions \
  --service-id $SVC_ID \
  --add-allowed-principals \
    arn:aws:iam::111122223333:root \
    arn:aws:iam::444455556666:role/MCPConsumerRole

# Remove a principal from allowlist (they can no longer create new endpoints)
aws ec2 modify-vpc-endpoint-service-permissions \
  --service-id $SVC_ID \
  --remove-allowed-principals arn:aws:iam::111122223333:root

Revoking a principal from the allowlist does not terminate existing connections — it only prevents new endpoint creation. To terminate an active connection, use reject-vpc-endpoint-connections with the endpoint ID, which moves it to a "rejected" state and closes TCP connections within 30 seconds. Principals without explicit allowlist entries cannot create endpoints to your service even if they know the service name, as long as acceptance-required is true or the principal is not in the allowed list.

Private DNS for PrivateLink services

By default, consumers address the endpoint using the generated vpce-xxx.vpce-svc-xxx.region.vpce.amazonaws.com hostname. Providers can associate a private DNS name with the endpoint service so consumers who verify domain ownership can use a branded hostname instead. This requires ownership of a public DNS zone — AWS verifies the association with a TXT record.

# Associate a private DNS name with the endpoint service (provider side)
aws ec2 modify-vpc-endpoint-service-configuration \
  --service-id $SVC_ID \
  --private-dns-name mcp.provider-company.com

# AWS returns a verification TXT record to add to public DNS
aws ec2 describe-vpc-endpoint-service-configurations \
  --service-ids $SVC_ID \
  --query 'ServiceConfigurations[0].PrivateDnsNameConfiguration'
# Returns: {Name: "_some-token.mcp.provider-company.com",
#           Value: "vpce-abc123verify", Type: "TXT", State: "pendingVerification"}

# After adding the TXT record to your public DNS zone, start verification
aws ec2 start-vpc-endpoint-service-private-dns-verification \
  --service-id $SVC_ID

# Check verification status
aws ec2 describe-vpc-endpoint-service-configurations \
  --service-ids $SVC_ID \
  --query 'ServiceConfigurations[0].PrivateDnsNameConfiguration.State'
# "verified" when successful

# Consumer side: enable private DNS when creating the endpoint
aws ec2 create-vpc-endpoint \
  --vpc-endpoint-type Interface \
  --service-name com.amazonaws.vpce.us-east-1.vpce-svc-0abc123 \
  --vpc-id vpc-0consumer \
  --subnet-ids subnet-0consumer-a \
  --security-group-ids sg-0consumer-mcp \
  --private-dns-enabled

Once private DNS is verified and the consumer enables it on their endpoint, mcp.provider-company.com resolves to the endpoint ENI's private IP within the consumer VPC — no Route 53 private hosted zone setup needed on the consumer side. The consumer's existing Route 53 resolver handles it. If the consumer's VPC has split-horizon DNS or custom resolvers, ensure the custom resolver forwards the domain to Route 53 rather than intercepting it.

PrivateLink vs VPC peering — when to use each

FactorPrivateLinkVPC Peering
CIDR overlapWorks with overlapping CIDRs — no routing concernFails — peered VPCs cannot have overlapping CIDRs
Access scopeConsumer reaches specific service endpoint only — unidirectionalFull VPC-to-VPC route — any resource in either VPC can reach any other (scoped by security groups)
Cross-accountNative — service name is account-agnostic; principal allowlist controls accessRequires peering request/accept per account pair; does not work across AWS Organizations without explicit setup
On-premises accessNot directly accessible via VPN/Direct Connect from on-premisesAccessible via VPN/Direct Connect if peered VPC has connectivity
Data transfer cost$0.01/GB + $0.01/hr per AZ per endpoint; NLB processing fees apply$0.01/GB within the same region across different AZs (no charge within same AZ)
Transitive routingNot needed — endpoint service is directly in consumer VPC's DNSNot supported — A→B and B→C does not allow A→C

Failure modes reference

FailureSymptomFix
Endpoint stuck in pendingAcceptanceConsumer endpoint created but traffic fails; no connection establishedProvider must accept with accept-vpc-endpoint-connections; if acceptance-required=false check that consumer principal is in the allowed principals list
NLB health checks failingEndpoint service exists but consumer connections get "endpoint unavailable" or timeoutNLB target group must show healthy targets; verify MCP server is running and the target group health check (TCP port) succeeds; unhealthy NLB takes PrivateLink service offline
Private DNS name not resolving in consumer VPCBranded DNS name fails; consumers use vpce-xxx hostname successfullyConsumer must set private-dns-enabled=true when creating endpoint; provider DNS name must be in "verified" state; check with describe-vpc-endpoint-service-configurations
TLS certificate mismatch for custom DNSSSL handshake error when consumer uses branded DNS nameNLB TLS listener certificate must include the branded DNS name (mcp.provider-company.com) as a SAN; ACM certificate in the same region as the NLB
Source IP shows NLB private IP, not consumer IPMCP server logs show 10.x NLB IPs; cannot distinguish consumersEnable Proxy Protocol v2 on the NLB target group and parse the proxy protocol header in the MCP server; or use the endpoint connection ID in access logs for correlation