⚠️ IMPORTANT: This is a PROOF OF CONCEPT / SAMPLE IMPLEMENTATION for demonstration and learning purposes only. DO NOT USE IN PRODUCTION without significant security hardening and enterprise-grade enhancements. This sample demonstrates architectural patterns and best practices but requires additional work for production deployment.
A comprehensive TypeScript implementation demonstrating Amazon Bedrock AgentCore with multi-tenant log analytics, showcasing advanced agentic architecture patterns with proper security isolation and identity propagation.
This project implements a sample multi-tenant log analytics system using AWS Bedrock AgentCore. It demonstrates how to build sophisticated AI agents that can access multiple tool sources while maintaining tenant isolation and security boundaries. This is intended as an educational resource and reference implementation for developers learning to build multi-tenant AI agent systems.
- Multi-Tenant Architecture: Complete tenant isolation using JWT claims and OpenSearch query filters
- Dual Tool Sources: Combines simple MCP server tools with complex Gateway-based Lambda functions
- Identity Propagation: Automatic tenant ID extraction and injection throughout the request flow
- Production-Ready Security: Cognito authentication, JWT validation, and fine-grained access control
- Full TypeScript Stack: Type-safe implementation across all components
- Infrastructure as Code: Complete AWS CDK deployment with 9 integrated stacks
╔═══════════════════════════════════════════════════════════════════════╗
║ INTERNET AREA ║
╠═══════════════════════════════════════════════════════════════════════╣
║ ║
║ ┌─────────────────────────────────────────────────────────────────┐ ║
║ │ User Application │ ║
║ │ (Authenticates with Cognito JWT Token) │ ║
║ └───────────────────────────┬─────────────────────────────────────┘ ║
║ │ Authorization: Bearer <ID_TOKEN> ║
║ │ (contains custom:tenantId claim) ║
║ ▼ ║
║ ┌─────────────────────────────────────────────────────────────────┐ ║
║ │ AgentCore Runtime (Agent) │ ║
║ │ (HTTP Protocol, Public Network) │ ║
║ │ ┌────────────────────────────────────────────────────────────┐ │ ║
║ │ │ • Strands-based orchestration agent │ │ ║
║ │ │ • Cognito JWT authentication │ │ ║
║ │ │ • Extracts tenant ID from JWT token │ │ ║
║ │ │ • Routes to appropriate tools based on query complexity │ │ ║
║ │ └────────────────────────────────────────────────────────────┘ │ ║
║ └─────────────┬──────────────────────────────┬────────────────────┘ ║
║ │ │ ║
║ │ │ ║
║ │ ┌──────────▼──────────────┐ ║
║ │ │ AgentCore Gateway │ ║
║ │ │ (Lambda Tools) │ ║
║ │ └──────────┬──────────────┘ ║
║ │ │ ║
║ │ ┌──────────▼──────────────┐ ║
║ │ │ Request Interceptor │ ║
║ │ │ (JWT → tenantId) │ ║
║ │ └──────────┬──────────────┘ ║
║ │ │ ║
╚════════════════╪══════════════════════════════╪══════════════════════╝
│ │
╔════════════════╪══════════════════════════════╪══════════════════════╗
║ │ PRIVATE AREA (VPC) │ ║
╠════════════════╪══════════════════════════════╪══════════════════════╣
║ │ │ ║
║ ┌─────────▼─────────────────┐ │ ║
║ │ MCP Server (AgentCore) │ │ ║
║ │ • Simple Tools │ │ ║
║ │ - search-logs │ │ ║
║ │ - get-log-stats │ │ ║
║ └─────────┬─────────────────┘ │ ║
║ │ │ ║
║ │ ┌──────────▼───────────────┐ ║
║ │ │ Lambda Function (VPC) │ ║
║ │ │ • analyze-errors │ ║
║ │ │ • trace-request │ ║
║ │ └──────────┬───────────────┘ ║
║ │ │ ║
║ └──────────────┬───────────────┘ ║
║ │ ║
║ ┌────────▼─────────┐ ║
║ │ OpenSearch │ ║
║ │ (Tenant-Filtered)│ ║
║ └──────────────────┘ ║
║ ║
╚══════════════════════════════════════════════════════════════════════╝
- Amazon Bedrock AgentCore: Agent hosting and orchestration platform
- Amazon Bedrock: AI model inference (Nova 2 Lite)
- Amazon OpenSearch Service: Multi-tenant log storage and analytics
- Amazon Cognito: User authentication with custom tenant attributes
- AWS Lambda: Serverless compute for complex analytics tools
- TypeScript 5.3+: Type-safe development across all components
- Node.js 20+: Runtime environment
- AWS CDK 2.170+: Infrastructure as code
- Strands Agent SDK: Agent orchestration framework
- Model Context Protocol (MCP): Tool integration protocol
- Express.js: HTTP server framework
- Zod: Runtime type validation
- Amazon VPC: Network isolation for OpenSearch
- AWS Secrets Manager: Secure credential storage
- Amazon API Gateway: OpenSearch proxy for data seeding
- AWS IAM: Fine-grained access control
- Amazon CloudWatch: Logging and monitoring
agentcore-multi-tenant/
├── packages/
│ ├── agent/ # Strands Agent (AgentCore Runtime)
│ │ ├── src/
│ │ │ ├── agent.ts # Agent configuration and tool setup
│ │ │ ├── index.ts # HTTP server and request handling
│ │ │ ├── auth.ts # JWT token parsing and validation
│ │ │ └── types.ts # Type definitions
│ │ ├── Dockerfile # Container image definition
│ │ └── package.json # Dependencies and scripts
│ │
│ ├── mcp-server/ # MCP Server (AgentCore Runtime)
│ │ ├── src/
│ │ │ ├── server.ts # MCP tool definitions
│ │ │ ├── opensearch.ts # Direct OpenSearch queries
│ │ │ └── index.ts # Server initialization
│ │ ├── Dockerfile # Container image definition
│ │ └── package.json # Dependencies and scripts
│ │
│ ├── lambda/ # Gateway Lambda Functions
│ │ ├── src/
│ │ │ ├── analyze-errors.ts # Error pattern analysis
│ │ │ ├── trace-request.ts # Distributed tracing
│ │ │ ├── opensearch.ts # OpenSearch client utilities
│ │ │ ├── types.ts # Type definitions
│ │ │ └── index.ts # Lambda entry point
│ │ └── package.json # Dependencies and scripts
│ │
│ ├── opensearch-proxy/ # OpenSearch API Gateway Proxy
│ │ ├── src/
│ │ │ └── index.ts # Proxy handler for bulk/search operations
│ │ └── package.json # Dependencies and scripts
│ │
│ └── infrastructure/ # AWS CDK Infrastructure
│ ├── lib/
│ │ ├── vpc-stack.ts # VPC, subnets, NAT Gateway
│ │ ├── cognito-stack.ts # User pool, custom attributes
│ │ ├── opensearch-stack.ts # OpenSearch domain
│ │ ├── opensearch-proxy-stack.ts # API Gateway for data seeding
│ │ ├── opensearch-config-stack.ts # Index configuration
│ │ ├── lambda-stack.ts # Analytics Lambda functions
│ │ ├── gateway-stack.ts # AgentCore Gateway
│ │ ├── mcp-stack.ts # MCP Server Runtime
│ │ └── agent-stack.ts # Agent Runtime
│ ├── bin/
│ │ └── app.ts # CDK app entry point
│ └── package.json # Dependencies and scripts
│
├── scripts/
│ ├── configure-gateway-interceptor.py # Gateway interceptor setup
│ └── test-jwt-token.sh # JWT token testing utility
│
├── package.json # Root workspace configuration
├── tsconfig.json # TypeScript configuration
└── README.md # This file
- Node.js: >= 20.0.0
- npm: >= 10.0.0
- AWS CLI: Configured with appropriate credentials
- Docker: For building container images
- AWS CDK CLI: Install globally with
npm install -g aws-cdk
- Amazon Bedrock (with model access for Claude or Nova)
- Amazon Bedrock AgentCore
- Amazon OpenSearch Service
- Amazon Cognito
- AWS Lambda
- Amazon VPC
- AWS IAM (permissions to create roles and policies)
- Sufficient service quotas for VPC resources
- Permissions to create IAM roles and policies
- Access to create Bedrock AgentCore resources
- Budget for ongoing AWS service costs
git clone git@ssh.gitlab.aws.dev:flolac/agentcore-multi-tenant.git
cd agentcore-multi-tenantInstall all workspace dependencies:
npm installCompile TypeScript code across all packages:
npm run buildThis compiles:
- Agent package
- MCP server package
- Lambda functions
- Infrastructure code
If you haven't used CDK in your AWS account/region before:
cd packages/infrastructure
cdk bootstrapDeploy the complete infrastructure:
cd packages/infrastructure
cdk deploy --allThis deploys 9 stacks in the correct order:
- VPC Stack
- Cognito Stack
- OpenSearch Stack
- OpenSearch Proxy Stack
- OpenSearch Config Stack
- Lambda Stack
- Gateway Stack
- MCP Stack
- Agent Stack
Deployment takes approximately 20-30 minutes due to OpenSearch domain creation.
After deployment, configure the Gateway Request Interceptor:
cd scripts
python3 configure-gateway-interceptor.pyCreate users with tenant IDs:
# Get User Pool ID from CDK outputs
USER_POOL_ID=$(aws cloudformation describe-stacks \
--stack-name AgentCoreTypescriptSample-CognitoStack \
--query 'Stacks[0].Outputs[?OutputKey==`UserPoolId`].OutputValue' \
--output text)
# Create user for tenant-123
aws cognito-idp admin-create-user \
--user-pool-id "$USER_POOL_ID" \
--username alice \
--user-attributes \
Name=email,Value=alice@example.com \
Name=custom:tenantId,Value=tenant-123 \
--temporary-password TempPass123! \
--message-action SUPPRESS
# Create user for tenant-456
aws cognito-idp admin-create-user \
--user-pool-id "$USER_POOL_ID" \
--username bob \
--user-attributes \
Name=email,Value=bob@example.com \
Name=custom:tenantId,Value=tenant-456 \
--temporary-password TempPass456! \
--message-action SUPPRESSUse the OpenSearch proxy to seed test data:
# Get proxy endpoint from CDK outputs
PROXY_ENDPOINT=$(aws cloudformation describe-stacks \
--stack-name AgentCoreTypescriptSample-OpenSearchProxyStack \
--query 'Stacks[0].Outputs[?OutputKey==`ProxyEndpoint`].OutputValue' \
--output text)
# Create sample logs for tenant-123
curl -X POST "$PROXY_ENDPOINT/logs/_bulk" \
-H "Content-Type: application/x-ndjson" \
--data-binary @sample-logs-tenant-123.ndjson
# Create sample logs for tenant-456
curl -X POST "$PROXY_ENDPOINT/logs/_bulk" \
-H "Content-Type: application/x-ndjson" \
--data-binary @sample-logs-tenant-456.ndjsonGet an ID token from Cognito:
# Get User Pool Client ID
CLIENT_ID=$(aws cloudformation describe-stacks \
--stack-name AgentCoreTypescriptSample-CognitoStack \
--query 'Stacks[0].Outputs[?OutputKey==`UserPoolClientId`].OutputValue' \
--output text)
# Authenticate user
aws cognito-idp initiate-auth \
--client-id "$CLIENT_ID" \
--auth-flow USER_PASSWORD_AUTH \
--auth-parameters \
USERNAME=alice,PASSWORD=YourSecurePassword123!
# Extract IdToken from response
ID_TOKEN="<id_token_from_response>"Send requests to the agent:
# Get Agent URL from CDK outputs
AGENT_URL=$(aws cloudformation describe-stacks \
--stack-name AgentCoreTypescriptSample-AgentStack \
--query 'Stacks[0].Outputs[?OutputKey==`AgentUrl`].OutputValue' \
--output text)
# Simple query using MCP server tools
curl -X POST "$AGENT_URL/invocations" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ID_TOKEN" \
-d '{
"prompt": "Show me error logs from the last hour"
}'
# Complex analysis using Gateway tools
curl -X POST "$AGENT_URL/invocations" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ID_TOKEN" \
-d '{
"prompt": "Analyze error patterns and show me trends"
}'
# Distributed tracing
curl -X POST "$AGENT_URL/invocations" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ID_TOKEN" \
-d '{
"prompt": "Trace request abc-123-def-456"
}'The agent automatically selects appropriate tools:
-
Simple queries → MCP Server tools (direct OpenSearch)
- "show me logs with error"
- "what are the log statistics"
-
Complex analysis → Gateway Lambda tools
- "analyze error patterns"
- "what trends do errors show"
- "trace request XYZ"
1. User authenticates with Cognito
↓
2. Pre-Token Generation Lambda adds claims to ID Token:
• custom:tenantId (from user attributes)
• client_id (required by AgentCore)
↓
3. Application sends request to Agent with ID Token
Authorization: Bearer <ID_TOKEN>
↓
4. AgentCore Runtime validates JWT token
• Verifies signature
• Checks client_id claim
• Validates expiration
↓
5. Agent extracts tenant ID from JWT token
• Parses JWT payload
• Extracts custom:tenantId claim
• Stores in agent state
↓
6. Agent invokes tools with tenant context:
│
├─→ MCP Server
│ • Authorization header propagated
│ • Tenant ID available in context
│ • Direct OpenSearch queries
│
└─→ Gateway
• Request Interceptor Lambda triggered
• Extracts custom:tenantId from JWT
• Automatically injects into tool arguments
• Lambda receives pre-filled tenant ID
↓
7. OpenSearch queries filtered by tenantId
↓
8. User sees only their tenant's data
- Cognito JWT tokens: Industry-standard authentication
- Custom tenant attributes: Tenant ID embedded in verified tokens
- Client ID validation: AgentCore validates token authenticity
- Token expiration: Time-limited access tokens
- JWT-based tenant ID: Extracted from verified token, not user input
- Gateway Request Interceptor: Automatic tenant ID injection
- Query-time filtering: All OpenSearch queries include tenant filter
- Centralized enforcement: Cannot be bypassed by agent or user
- VPC isolation: OpenSearch in private subnets
- Security groups: Restricted network access
- HTTPS enforcement: All traffic encrypted in transit
- Private connectivity: No direct internet access to data stores
- Fine-grained IAM: Least-privilege service roles
- Resource-level permissions: Scoped to specific resources
- Audit trails: CloudWatch Logs for all operations
- Secrets management: Credentials in AWS Secrets Manager
Each package can be developed independently:
# Agent package
cd packages/agent
npm run dev
# MCP Server package
cd packages/mcp-server
npm run dev
# Lambda package (unit tests)
cd packages/lambda
npm test
# Infrastructure (synth only)
cd packages/infrastructure
npm run synth# Run all tests
npm test
# Test specific package
cd packages/lambda
npm test
# Test JWT token extraction
cd scripts
./test-jwt-token.shEdit /packages/mcp-server/src/server.ts:
mcpServer.registerTool(
'my-new-tool',
{
title: 'My New Tool',
description: 'Tool description',
inputSchema: {
param1: z.string().describe('Parameter description'),
},
},
async ({ param1 }, context) => {
// Tool implementation
return {
content: [{ type: 'text', text: 'Result' }],
};
}
);- Create handler in
/packages/lambda/src/my-tool.ts - Update
/packages/infrastructure/lib/gateway-stack.tsto add tool definition - Redeploy:
cdk deploy GatewayStack
This sample creates AWS resources that incur costs:
- AgentCore Runtime: Per-hour instance costs
- OpenSearch domain: t3.small.search instance ($0.036/hour)
- NAT Gateway: $0.045/hour + data transfer
- Lambda invocations: Per-request and duration
- Bedrock model usage: Per-token pricing
- VPC resources: Negligible costs
- CloudWatch Logs: Log storage and ingestion
Estimated monthly cost: $50-100 for development/testing workloads
See AWS Pricing for detailed information.
Remove all deployed resources:
cd packages/infrastructure
cdk destroy --allThis removes:
- All CloudFormation stacks
- OpenSearch domain (data is deleted)
- Lambda functions
- AgentCore resources
- VPC and networking resources
- Cognito User Pool
Note: Some resources like CloudWatch Logs may persist and incur minimal costs.
- Verify JWT token is valid and not expired
- Ensure token includes
custom:tenantIdclaim - Check user has
custom:tenantIdattribute set in Cognito - Use ID Token, not Access Token
- Verify sample data was seeded for the correct tenant ID
- Check OpenSearch index exists
- Review Lambda CloudWatch Logs for errors
- Verify Lambda is in VPC with OpenSearch security group
- Check security group allows inbound traffic from Lambda
- Verify OpenSearch domain is active and healthy
- Run
scripts/configure-gateway-interceptor.py - Verify Lambda has correct permissions
- Check CloudWatch Logs for interceptor Lambda
- Verify AWS CLI credentials are configured
- Check CDK bootstrap completed successfully
- Ensure service quotas are sufficient
- Review CloudFormation events for specific errors
- TESTING.md: Testing guide and scenarios
- Amazon Bedrock AgentCore Documentation
- Strands Agent Framework
- Model Context Protocol
- MCP TypeScript SDK
- AWS CDK Developer Guide
- Amazon OpenSearch Service
- Amazon Cognito Developer Guide
This is a sample project for demonstration and educational purposes. Contributions that improve the sample implementation or documentation are welcome:
- Bug fixes and improvements to demonstration code
- Documentation enhancements and clarifications
- Additional example tools or use cases
- Testing improvements and scenarios
MIT License - See LICENSE file for details.
For issues or questions:
- Review TESTING.md for testing scenarios
- Check AWS service health dashboard
- Review CloudWatch Logs for errors
- Contact AWS Support for service-specific issues