1
0
mirror of synced 2026-05-22 22:53:35 +00:00

Adding required Lib folder for blueprints. (#797)

* Fix wording typo in notebook about user consent flow

cosmetic update

Signed-off-by: Hardik Thakkar <68253981+HardikThakkar94@users.noreply.github.com>

* Add pyyaml to requirements.txt

Signed-off-by: Hardik Thakkar <68253981+HardikThakkar94@users.noreply.github.com>

* Add HardikThakkar94 to CONTRIBUTORS.md

Signed-off-by: Hardik Thakkar <68253981+HardikThakkar94@users.noreply.github.com>

* Updates to fix the Streamlit app access when running in sagemaker

Modified
- Requirements.txt (added dependencies)
- chatbot_app_cognito.py (added get_streamlit_url, for sagemaker access)
- runtime_with_strands_and_egress_3lo.ipynb (streamlit piece for access url, cosmetic updates)

* Fixing Ruff errors reported by python-lint

* removing Ruff errors from python-lint

* passing 3.7 as the model for workshop

* Docs: add prerequisites (OpenAI or Azure OpenAI) cell to Outbound Auth notebook

* Revert "Docs: add prerequisites (OpenAI or Azure OpenAI) cell to Outbound Auth notebook"

This reverts commit 5dded4c38a.

* Add prerequisites (OpenAI or Azure OpenAI) cell to Outbound Auth notebook

* cosmetic fix

* Updating OpenAI URL

* Added instructions on the OAuth flow session binding and Streamlit functionality

* All imports are now properly organized at the top of the file, following Python best practices (PEP 8). The linting errors should now be resolved:
  -  runtime.py:18:1: E402 - Fixed
  -  runtime.py:19:1: E402 - Fixed
  -  runtime.py:19:20: F811 - Fixed
  -  runtime.py:25:1: E402 - Fixed

* formatting fixed

* Update Identity Outbound tutorial notebooks with corrections and improvements:
1. 05-Outbound_Auth_3lo notebook: Fixed credential provider name typo
2. 06-Outbound_Auth_Github notebook: Multiple improvements including:
    - Updated description text for GitHub-specific use case
    - Reorganized imports (moved to top of cell)
    - Added boto session and region setup
    - Reordered OAuth flow description
    - Restructured notebook sections (removed redundant policy section, added clearer status check and invoke sections)
    - Fixed credential provider name reference

* Fixed Identity Sections based on SageMaker (Workshop) to handle oauth2_callback_server and other cosmetic updates.

* Remove unused import and added permissions for 1st time model access for workshops

* formatting fixed.

* parameterize provider, update github image.

* added import boto3 and updated image for GitHub Session Binding

* Update Model and Remove Global Var

* Travel and Shopping concierge agents blueprints

* add missing contributors for the blueprint

* fix python-lint errors

* CodeQL fixes and config

* fix python-lint unused imports

* fix python-lint

* fix linter and cql issues

* run linter

* update codeql suppressions

* suppress codeql

* Revert accidental changes to 01-tutorials and 03-integrations

Remove files accidentally added to 01-tutorials and 03-integrations in previous commits.
These changes were not intended to be part of the blueprint additions.

Reverted files:
- 01-tutorials/03-AgentCore-identity/06-Outbound_Auth_Github/.dockerignore
- 01-tutorials/03-AgentCore-identity/06-Outbound_Auth_Github/Dockerfile
- 01-tutorials/03-AgentCore-identity/06-Outbound_Auth_Github/github_agent.py
- 03-integrations/IDP-examples/EntraID/.agentcore.json
- 03-integrations/IDP-examples/EntraID/.dockerignore
- 03-integrations/IDP-examples/EntraID/Dockerfile
- 03-integrations/IDP-examples/EntraID/strands_entraid_onenote.py

* fix formatting

* Update 05-blueprints/shopping-concierge-agent/tests/utils.py

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Hardik Thakkar <68253981+HardikThakkar94@users.noreply.github.com>

* removed tests folders.

* remove info logging

* remove logging

* codeql suppressions

* Update server.py

# codeql[py/clear-text-logging-sensitive-data] Debug logging for certificate verification - logs metadata only, not private key content

Signed-off-by: Hardik Thakkar <68253981+HardikThakkar94@users.noreply.github.com>

* Updating .gitignore and adding lib folder required for the shopping and travel concierge agents

---------

Signed-off-by: Hardik Thakkar <68253981+HardikThakkar94@users.noreply.github.com>
Co-authored-by: HT <hardikvt@amazon.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Hardik Thakkar
2025-12-22 11:30:28 -05:00
committed by GitHub
parent c2ea97d283
commit bc6a8f161c
22 changed files with 1980 additions and 0 deletions
+1
View File
@@ -49,6 +49,7 @@ downloads/
eggs/
.eggs/
lib/
!05-blueprints/**/lib/
lib64/
parts/
sdist/
@@ -0,0 +1,298 @@
import * as cdk from 'aws-cdk-lib';
import * as agentcore from '@aws-cdk/aws-bedrock-agentcore-alpha';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as cognito from 'aws-cdk-lib/aws-cognito';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as customResources from 'aws-cdk-lib/custom-resources';
import { Construct } from 'constructs';
import { GatewayConstruct } from './constructs/gateway-construct';
import * as path from 'path';
import * as fs from 'fs';
// Sanitize names for AgentCore resources (alphanumeric + underscores)
const sanitizeName = (name: string) =>
name.toLowerCase().replace(/[^a-z0-9]/g, '_').replace(/_+/g, '_');
// Load deployment config
const deploymentConfig = JSON.parse(fs.readFileSync('../../deployment-config.json', 'utf-8'));
const DEPLOYMENT_ID = deploymentConfig.deploymentId;
export class AgentStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// Add deployment info
cdk.Tags.of(this).add('DeploymentId', DEPLOYMENT_ID);
cdk.Tags.of(this).add('DeploymentName', deploymentConfig.deploymentName || DEPLOYMENT_ID);
// Import Cognito from Amplify
const userPoolId = cdk.Fn.importValue(`ConciergeAgent-${DEPLOYMENT_ID}-Auth-UserPoolId`);
const clientId = cdk.Fn.importValue(`ConciergeAgent-${DEPLOYMENT_ID}-Auth-ClientId`);
const cognitoRegion = cdk.Fn.importValue(`ConciergeAgent-${DEPLOYMENT_ID}-Auth-Region`);
const discoveryUrl = `https://cognito-idp.${cognitoRegion}.amazonaws.com/${userPoolId}/.well-known/openid-configuration`;
// Import machine client ID from Amplify (created there for proper deployment order)
const machineClientId = cdk.Fn.importValue(`ConciergeAgent-${DEPLOYMENT_ID}-Auth-MachineClientId`);
// Import the user pool for OAuth provider secret retrieval
const userPool = cognito.UserPool.fromUserPoolId(this, 'ImportedUserPool', userPoolId);
// Import DynamoDB tables
const userProfileTableName = cdk.Fn.importValue(`ConciergeAgent-${DEPLOYMENT_ID}-Data-UserProfileTableName`);
const wishlistTableName = cdk.Fn.importValue(`ConciergeAgent-${DEPLOYMENT_ID}-Data-WishlistTableName`);
const feedbackTableName = cdk.Fn.importValue(`ConciergeAgent-${DEPLOYMENT_ID}-Data-FeedbackTableName`);
// Import MCP runtime ARNs (using deployment ID in stack names)
const cartRuntimeArn = cdk.Fn.importValue(`CartStack-${DEPLOYMENT_ID}-RuntimeArn`);
const shoppingRuntimeArn = cdk.Fn.importValue(`ShoppingStack-${DEPLOYMENT_ID}-RuntimeArn`);
// 1. Create Custom Resource Lambda for OAuth Provider
const oauthProviderRole = new iam.Role(this, 'OAuthProviderLambdaRole', {
assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
description: 'Execution role for OAuth Provider Custom Resource Lambda',
managedPolicies: [
iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole')
],
inlinePolicies: {
OAuthProviderPolicy: new iam.PolicyDocument({
statements: [
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: [
'bedrock-agentcore:CreateOAuth2CredentialProvider',
'bedrock-agentcore:DeleteOAuth2CredentialProvider',
'bedrock-agentcore:ListOAuth2CredentialProviders',
'bedrock-agentcore:GetOAuth2CredentialProvider',
'bedrock-agentcore:CreateTokenVault', // Required for OAuth provider creation
'bedrock-agentcore:DeleteTokenVault',
'bedrock-agentcore:GetTokenVault',
'secretsmanager:CreateSecret', // Required to store OAuth client secret
'secretsmanager:DeleteSecret',
'secretsmanager:GetSecretValue',
'secretsmanager:PutSecretValue',
'cognito-idp:DescribeUserPoolClient' // Required to fetch client secret
],
resources: ['*']
})
]
})
}
});
const oauthProviderLambda = new lambda.Function(this, 'OAuthProviderLambda', {
runtime: lambda.Runtime.PYTHON_3_13,
handler: 'index.handler',
code: lambda.Code.fromAsset(path.join(__dirname, '..', 'lambdas', 'oauth-provider')),
timeout: cdk.Duration.minutes(5),
role: oauthProviderRole,
description: 'Custom Resource for OAuth2 Credential Provider management'
});
const oauthProvider = new customResources.Provider(this, 'OAuthProvider', {
onEventHandler: oauthProviderLambda
});
// 2. Create OAuth Provider via Custom Resource
const oauthCredentialProvider = new cdk.CustomResource(this, 'OAuthCredentialProvider', {
serviceToken: oauthProvider.serviceToken,
properties: {
ProviderName: sanitizeName(`oauth_provider_${this.stackName}`),
UserPoolId: userPoolId,
ClientId: machineClientId,
DiscoveryUrl: discoveryUrl,
Version: '2' // Increment to force update
}
});
const oauthProviderArn = oauthCredentialProvider.getAttString('ProviderArn');
// 1. Create Memory using L2 construct
const memory = new agentcore.Memory(this, 'Memory', {
memoryName: sanitizeName(`memory_${this.stackName}`),
description: 'Short-term memory for Concierge Agent',
});
// 2. Create execution role for main agent
const agentRole = new iam.Role(this, 'AgentRole', {
assumedBy: new iam.ServicePrincipal('bedrock-agentcore.amazonaws.com'),
description: 'Execution role for Concierge Agent'
});
// Grant DynamoDB permissions
agentRole.addToPolicy(new iam.PolicyStatement({
actions: ['dynamodb:GetItem', 'dynamodb:Scan', 'dynamodb:UpdateItem', 'dynamodb:Query', 'dynamodb:PutItem', 'dynamodb:DeleteItem', 'dynamodb:BatchWriteItem'],
resources: [
`arn:aws:dynamodb:${this.region}:${this.account}:table/${userProfileTableName}`,
`arn:aws:dynamodb:${this.region}:${this.account}:table/${userProfileTableName}/index/*`,
`arn:aws:dynamodb:${this.region}:${this.account}:table/${wishlistTableName}`,
`arn:aws:dynamodb:${this.region}:${this.account}:table/${wishlistTableName}/index/*`,
`arn:aws:dynamodb:${this.region}:${this.account}:table/${feedbackTableName}`,
`arn:aws:dynamodb:${this.region}:${this.account}:table/${feedbackTableName}/index/*`
]
}));
// Grant CloudWatch Logs permissions
agentRole.addToPolicy(new iam.PolicyStatement({
actions: ['logs:CreateLogGroup', 'logs:CreateLogStream', 'logs:PutLogEvents', 'logs:DescribeLogGroups', 'logs:DescribeLogStreams', 'logs:GetLogEvents', 'logs:FilterLogEvents'],
resources: [`arn:aws:logs:${this.region}:${this.account}:log-group:*`]
}));
// Grant Memory access
agentRole.addToPolicy(new iam.PolicyStatement({
actions: [
'bedrock-agentcore:GetMemory',
'bedrock-agentcore:ListMemories',
'bedrock-agentcore:CreateEvent',
'bedrock-agentcore:GetEvent',
'bedrock-agentcore:ListEvents',
'bedrock-agentcore:RetrieveMemoryRecords'
],
resources: [memory.memoryArn]
}));
// Grant Bedrock model access
agentRole.addToPolicy(new iam.PolicyStatement({
actions: ['bedrock:InvokeModel', 'bedrock:InvokeModelWithResponseStream'],
resources: [
`arn:aws:bedrock:*::foundation-model/*`,
`arn:aws:bedrock:*:${this.account}:inference-profile/*`
]
}));
// Grant Gateway invoke permissions (for runtime to call gateway)
agentRole.addToPolicy(new iam.PolicyStatement({
actions: ['bedrock-agentcore:InvokeGateway'],
resources: ['*'] // Will be restricted to specific gateway after creation
}));
// Grant ECR permissions (required for runtime to pull container image)
agentRole.addToPolicy(new iam.PolicyStatement({
actions: [
'ecr:GetAuthorizationToken',
'ecr:BatchCheckLayerAvailability',
'ecr:GetDownloadUrlForLayer',
'ecr:BatchGetImage'
],
resources: ['*']
}));
// Grant Cognito permissions (required for runtime to fetch machine client secret and domain)
agentRole.addToPolicy(new iam.PolicyStatement({
actions: [
'cognito-idp:DescribeUserPoolClient',
'cognito-idp:DescribeUserPool'
],
resources: [`arn:aws:cognito-idp:${this.region}:${this.account}:userpool/${userPoolId}`]
}));
// 3. Create main agent runtime using L2 construct
const baseEnvVars = {
MEMORY_ID: memory.memoryId,
USER_PROFILE_TABLE_NAME: userProfileTableName,
WISHLIST_TABLE_NAME: wishlistTableName,
FEEDBACK_TABLE_NAME: feedbackTableName,
DEPLOYMENT_ID: DEPLOYMENT_ID,
// Gateway M2M authentication credentials (for runtime to call gateway)
GATEWAY_CLIENT_ID: machineClientId,
GATEWAY_USER_POOL_ID: userPoolId,
GATEWAY_SCOPE: 'concierge-gateway/invoke',
};
// Create runtime first (without GATEWAY_URL since gateway doesn't exist yet)
const runtime = new agentcore.Runtime(this, 'Runtime', {
runtimeName: sanitizeName(`agent_${this.stackName}`),
agentRuntimeArtifact: agentcore.AgentRuntimeArtifact.fromAsset(
path.join(__dirname, '../../..', 'concierge_agent', 'supervisor_agent')
),
executionRole: agentRole,
protocolConfiguration: agentcore.ProtocolType.HTTP,
networkConfiguration: agentcore.RuntimeNetworkConfiguration.usingPublicNetwork(),
authorizerConfiguration: agentcore.RuntimeAuthorizerConfiguration.usingJWT(
discoveryUrl,
[clientId, machineClientId] // Accept user tokens (frontend) AND M2M tokens (gateway)
),
environmentVariables: baseEnvVars,
description: 'Concierge Agent Runtime'
});
// 6. Create Gateway with JWT inbound and OAuth outbound
// IMPORTANT: Gateway must use SAME M2M client as runtime and OAuth provider
// Inbound: Frontend authenticates to gateway using M2M JWT token
// Outbound: Gateway authenticates to targets using OAuth (same M2M client)
const gateway = new GatewayConstruct(this, 'Gateway', {
gatewayName: sanitizeName(`gateway_${this.stackName}`).replace(/_/g, '-'),
mcpRuntimeArns: [
{ name: 'CartTools', arn: cartRuntimeArn },
{ name: 'ShoppingTools', arn: shoppingRuntimeArn }
],
cognitoClientId: machineClientId, // Use M2M client (same as runtime and OAuth)
cognitoDiscoveryUrl: discoveryUrl,
oauthProviderArn: oauthProviderArn,
oauthScope: 'concierge-gateway/invoke'
});
// Store gateway URL in SSM Parameter Store for runtime to access
const gatewayUrlParameter = new cdk.aws_ssm.StringParameter(this, 'GatewayUrlParameter', {
parameterName: `/concierge-agent/${DEPLOYMENT_ID}/gateway-url`,
stringValue: gateway.gatewayUrl,
description: 'AgentCore Gateway URL for supervisor agent',
tier: cdk.aws_ssm.ParameterTier.STANDARD,
});
// Grant runtime permission to read SSM parameter
agentRole.addToPolicy(new iam.PolicyStatement({
actions: ['ssm:GetParameter'],
resources: [gatewayUrlParameter.parameterArn]
}));
// Outputs
new cdk.CfnOutput(this, 'MainRuntimeArn', {
value: runtime.agentRuntimeArn,
exportName: `${this.stackName}-MainRuntimeArn`
});
new cdk.CfnOutput(this, 'MainRuntimeId', {
value: runtime.agentRuntimeId,
exportName: `${this.stackName}-MainRuntimeId`
});
new cdk.CfnOutput(this, 'MemoryId', {
value: memory.memoryId,
exportName: `${this.stackName}-MemoryId`
});
new cdk.CfnOutput(this, 'GatewayUrl', {
value: gateway.gatewayUrl,
exportName: `${this.stackName}-GatewayUrl`,
description: 'Gateway URL for MCP client connections (IAM auth)'
});
new cdk.CfnOutput(this, 'GatewayId', {
value: gateway.gatewayId,
exportName: `${this.stackName}-GatewayId`,
description: 'Gateway ID'
});
new cdk.CfnOutput(this, 'GatewayArn', {
value: gateway.gatewayArn,
exportName: `${this.stackName}-GatewayArn`,
description: 'Gateway ARN'
});
new cdk.CfnOutput(this, 'GatewayTargetCount', {
value: gateway.targets.length.toString(),
description: 'Number of gateway targets configured'
});
new cdk.CfnOutput(this, 'MachineClientId', {
value: machineClientId,
description: 'Machine client ID for gateway OAuth and MCP authentication (imported from Amplify)'
});
new cdk.CfnOutput(this, 'OAuthProviderArn', {
value: oauthProviderArn,
exportName: `${this.stackName}-OAuthProviderArn`,
description: 'OAuth provider ARN for gateway targets'
});
}
}
@@ -0,0 +1,21 @@
#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import * as fs from 'fs';
import { AgentStack } from './agent-stack';
// Load deployment config
const deploymentConfig = JSON.parse(fs.readFileSync('../../deployment-config.json', 'utf-8'));
const DEPLOYMENT_ID = deploymentConfig.deploymentId;
const app = new cdk.App();
new AgentStack(app, `AgentStack-${DEPLOYMENT_ID}`, {
env: {
account: process.env.CDK_DEFAULT_ACCOUNT,
region: process.env.CDK_DEFAULT_REGION || 'us-east-1'
},
description: `Concierge Agent with Gateway - Main agent runtime, memory, and gateway with MCP targets (${DEPLOYMENT_ID})`
});
app.synth();
@@ -0,0 +1,173 @@
import * as cdk from 'aws-cdk-lib';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as bedrockagentcore from 'aws-cdk-lib/aws-bedrockagentcore';
import { Construct } from 'constructs';
export interface GatewayProps {
gatewayName: string;
mcpRuntimeArns: { name: string; arn: string }[];
cognitoClientId: string;
cognitoDiscoveryUrl: string;
oauthProviderArn: string;
oauthScope: string;
}
export class GatewayConstruct extends Construct {
public readonly gateway: bedrockagentcore.CfnGateway;
public readonly gatewayArn: string;
public readonly gatewayId: string;
public readonly gatewayUrl: string;
public readonly targets: bedrockagentcore.CfnGatewayTarget[];
constructor(scope: Construct, id: string, props: GatewayProps) {
super(scope, id);
const stack = cdk.Stack.of(this);
// Gateway Role with permissions to invoke runtimes and Lambda functions
const gatewayRole = new iam.Role(this, 'GatewayRole', {
assumedBy: new iam.ServicePrincipal('bedrock-agentcore.amazonaws.com'),
description: 'Execution role for AgentCore Gateway',
inlinePolicies: {
GatewayPolicy: new iam.PolicyDocument({
statements: [
// Bedrock AgentCore permissions (only if there are MCP runtimes)
...(props.mcpRuntimeArns.length > 0 ? [new iam.PolicyStatement({
sid: 'BedrockAgentCoreAccess',
effect: iam.Effect.ALLOW,
actions: [
'bedrock-agentcore:InvokeRuntime',
'bedrock-agentcore:InvokeRuntimeWithResponseStream'
],
resources: [
...props.mcpRuntimeArns.map(r => r.arn)
],
})] : []),
// Lambda invoke permissions (for future Lambda targets)
new iam.PolicyStatement({
sid: 'LambdaInvokeAccess',
effect: iam.Effect.ALLOW,
actions: ['lambda:InvokeFunction'],
resources: [`arn:aws:lambda:${stack.region}:${stack.account}:function:*`],
}),
// Bedrock model access (for gateway operations)
new iam.PolicyStatement({
sid: 'BedrockModelAccess',
effect: iam.Effect.ALLOW,
actions: ['bedrock:InvokeModel', 'bedrock:InvokeModelWithResponseStream'],
resources: [
`arn:aws:bedrock:*::foundation-model/*`,
`arn:aws:bedrock:*:${stack.account}:inference-profile/*`
],
}),
// CloudWatch Logs
new iam.PolicyStatement({
sid: 'CloudWatchLogsAccess',
effect: iam.Effect.ALLOW,
actions: [
'logs:CreateLogGroup',
'logs:CreateLogStream',
'logs:PutLogEvents'
],
resources: [`arn:aws:logs:${stack.region}:${stack.account}:log-group:/aws/bedrock-agentcore/*`],
}),
// OAuth Provider access (for authenticating to MCP servers)
new iam.PolicyStatement({
sid: 'OAuthProviderAccess',
effect: iam.Effect.ALLOW,
actions: [
'bedrock-agentcore:GetOAuth2CredentialProvider',
'bedrock-agentcore:GetTokenVault',
'bedrock-agentcore:GetWorkloadAccessToken', // Required for OAuth workload identity
'bedrock-agentcore:GetResourceOauth2Token', // Required to get OAuth token from provider
'secretsmanager:GetSecretValue'
],
resources: [
props.oauthProviderArn,
`arn:aws:secretsmanager:${stack.region}:${stack.account}:secret:*`,
`arn:aws:bedrock-agentcore:${stack.region}:${stack.account}:workload-identity-directory/*`,
`arn:aws:bedrock-agentcore:${stack.region}:${stack.account}:token-vault/*`
],
}),
],
}),
},
});
// Create Gateway with JWT inbound auth
this.gateway = new bedrockagentcore.CfnGateway(this, 'Gateway', {
name: props.gatewayName,
roleArn: gatewayRole.roleArn,
protocolType: 'MCP',
protocolConfiguration: {
mcp: {
supportedVersions: ['2025-03-26']
// searchType removed - cannot be updated when targets are attached
}
},
authorizerType: 'CUSTOM_JWT',
authorizerConfiguration: {
customJwtAuthorizer: {
allowedClients: [props.cognitoClientId],
discoveryUrl: props.cognitoDiscoveryUrl
}
},
description: 'AgentCore Gateway with MCP protocol, JWT inbound auth, and OAuth outbound auth'
});
this.gatewayArn = this.gateway.attrGatewayArn;
this.gatewayId = this.gateway.attrGatewayIdentifier;
this.gatewayUrl = this.gateway.attrGatewayUrl;
// Create targets for MCP servers only
// Note: Main runtime calls the gateway, not the other way around
this.targets = [];
// MCP server targets with OAuth credentials
props.mcpRuntimeArns.forEach((mcpRuntime, index) => {
const mcpRuntimeUrl = this.constructRuntimeUrl(mcpRuntime.arn, stack.region);
const mcpTarget = new bedrockagentcore.CfnGatewayTarget(this, `McpTarget${index}`, {
gatewayIdentifier: this.gatewayId,
name: mcpRuntime.name.toLowerCase().replace(/[^a-z0-9-]/g, '-'),
description: `MCP Server: ${mcpRuntime.name}`,
targetConfiguration: {
mcp: {
mcpServer: {
endpoint: mcpRuntimeUrl
}
}
},
credentialProviderConfigurations: [{
credentialProviderType: 'OAUTH',
credentialProvider: {
oauthCredentialProvider: {
providerArn: props.oauthProviderArn,
scopes: [props.oauthScope]
}
}
}]
});
mcpTarget.addDependency(this.gateway);
this.targets.push(mcpTarget);
});
// Note: Outputs are created in the parent stack to avoid duplicate exports
}
/**
* Constructs the runtime invocation URL with properly encoded ARN
* Format: https://bedrock-agentcore.{region}.amazonaws.com/runtimes/{encoded-arn}/invocations
*
* Note: ARN encoding is handled by CloudFormation Fn::Join with URL-encoded characters
*/
private constructRuntimeUrl(runtimeArn: string, region: string): string {
// Use CloudFormation Fn::Join to construct URL with encoded ARN
// The ARN needs to be URL-encoded: : becomes %3A and / becomes %2F
return cdk.Fn.join('', [
`https://bedrock-agentcore.${region}.amazonaws.com/runtimes/`,
// Split ARN by : and / and rejoin with encoded versions
cdk.Fn.join('%2F', cdk.Fn.split('/', cdk.Fn.join('%3A', cdk.Fn.split(':', runtimeArn)))),
'/invocations'
]);
}
}
@@ -0,0 +1,21 @@
#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import * as fs from 'fs';
import { FrontendStack } from './frontend-stack';
// Load deployment config
const deploymentConfig = JSON.parse(fs.readFileSync('../../deployment-config.json', 'utf-8'));
const DEPLOYMENT_ID = deploymentConfig.deploymentId;
const app = new cdk.App();
new FrontendStack(app, `FrontendStack-${DEPLOYMENT_ID}`, {
env: {
account: process.env.CDK_DEFAULT_ACCOUNT,
region: process.env.CDK_DEFAULT_REGION || 'us-east-1'
},
description: `Concierge Agent Frontend - Amplify Hosting (${DEPLOYMENT_ID})`
});
app.synth();
@@ -0,0 +1,85 @@
import * as cdk from 'aws-cdk-lib';
import * as amplify from '@aws-cdk/aws-amplify-alpha';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as iam from 'aws-cdk-lib/aws-iam';
import { Construct } from 'constructs';
import * as fs from 'fs';
// Load deployment config
const deploymentConfig = JSON.parse(fs.readFileSync('../../deployment-config.json', 'utf-8'));
const DEPLOYMENT_ID = deploymentConfig.deploymentId;
export class FrontendStack extends cdk.Stack {
public readonly amplifyApp: amplify.App;
public readonly amplifyUrl: string;
public readonly stagingBucket: s3.Bucket;
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// Add deployment info
cdk.Tags.of(this).add('DeploymentId', DEPLOYMENT_ID);
cdk.Tags.of(this).add('DeploymentName', deploymentConfig.deploymentName || DEPLOYMENT_ID);
// Create staging bucket for Amplify deployments
this.stagingBucket = new s3.Bucket(this, 'StagingBucket', {
removalPolicy: cdk.RemovalPolicy.DESTROY,
autoDeleteObjects: true,
versioned: true,
publicReadAccess: false,
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
lifecycleRules: [
{
id: 'DeleteOldDeployments',
enabled: true,
expiration: cdk.Duration.days(30),
},
],
});
// Add bucket policy for Amplify access
this.stagingBucket.addToResourcePolicy(
new iam.PolicyStatement({
sid: 'AmplifyAccess',
effect: iam.Effect.ALLOW,
principals: [new iam.ServicePrincipal('amplify.amazonaws.com')],
actions: ['s3:GetObject', 's3:GetObjectVersion'],
resources: [this.stagingBucket.arnForObjects('*')],
})
);
// Create Amplify app
this.amplifyApp = new amplify.App(this, 'AmplifyApp', {
appName: 'concierge-agent-frontend',
description: 'Concierge Agent - React/Vite Frontend',
platform: amplify.Platform.WEB,
});
// Create main branch
this.amplifyApp.addBranch('main', {
stage: 'PRODUCTION',
branchName: 'main',
});
// Predictable domain format
this.amplifyUrl = `https://main.${this.amplifyApp.appId}.amplifyapp.com`;
// Outputs
new cdk.CfnOutput(this, 'AmplifyAppId', {
value: this.amplifyApp.appId,
description: 'Amplify App ID',
exportName: `${this.stackName}-AmplifyAppId`
});
new cdk.CfnOutput(this, 'AmplifyUrl', {
value: this.amplifyUrl,
description: 'Amplify App URL'
});
new cdk.CfnOutput(this, 'StagingBucketName', {
value: this.stagingBucket.bucketName,
description: 'S3 Staging Bucket Name',
exportName: `${this.stackName}-StagingBucketName`
});
}
}
@@ -0,0 +1,29 @@
#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import * as fs from 'fs';
import { CartStack } from './cart-stack';
import { ShoppingStack } from './shopping-stack';
// Load deployment config
const deploymentConfig = JSON.parse(fs.readFileSync('../../deployment-config.json', 'utf-8'));
const DEPLOYMENT_ID = deploymentConfig.deploymentId;
const app = new cdk.App();
const env = {
account: process.env.CDK_DEFAULT_ACCOUNT,
region: process.env.CDK_DEFAULT_REGION || 'us-east-1'
};
new CartStack(app, `CartStack-${DEPLOYMENT_ID}`, {
env,
description: `Cart MCP Server - Shopping cart tools (${DEPLOYMENT_ID})`
});
new ShoppingStack(app, `ShoppingStack-${DEPLOYMENT_ID}`, {
env,
description: `Shopping MCP Server - Product search tools (${DEPLOYMENT_ID})`
});
app.synth();
@@ -0,0 +1,139 @@
import * as cdk from 'aws-cdk-lib';
import * as agentcore from '@aws-cdk/aws-bedrock-agentcore-alpha';
import * as iam from 'aws-cdk-lib/aws-iam';
import { Construct } from 'constructs';
import * as path from 'path';
import * as fs from 'fs';
// Sanitize names for AgentCore resources (alphanumeric + underscores only)
const sanitizeName = (name: string) =>
name.toLowerCase().replace(/[^a-z0-9]/g, '_').replace(/_+/g, '_');
// Load deployment config
const deploymentConfig = JSON.parse(fs.readFileSync('../../deployment-config.json', 'utf-8'));
const DEPLOYMENT_ID = deploymentConfig.deploymentId;
export interface BaseMcpStackProps extends cdk.StackProps {
mcpName: string;
agentCodePath: string;
environmentVariables?: { [key: string]: string };
additionalPolicies?: iam.PolicyStatement[];
ssmParameters?: string[];
}
export abstract class BaseMcpStack extends cdk.Stack {
public readonly runtime: agentcore.Runtime;
public readonly role: iam.Role;
constructor(scope: Construct, id: string, props: BaseMcpStackProps) {
super(scope, id, props);
// Add deployment info
cdk.Tags.of(this).add('DeploymentId', DEPLOYMENT_ID);
cdk.Tags.of(this).add('DeploymentName', deploymentConfig.deploymentName || DEPLOYMENT_ID);
// Import Cognito configuration from Amplify stack
const userPoolId = cdk.Fn.importValue(`ConciergeAgent-${DEPLOYMENT_ID}-Auth-UserPoolId`);
const cognitoRegion = cdk.Fn.importValue(`ConciergeAgent-${DEPLOYMENT_ID}-Auth-Region`);
const machineClientId = cdk.Fn.importValue(`ConciergeAgent-${DEPLOYMENT_ID}-Auth-MachineClientId`);
const cognitoDiscoveryUrl = `https://cognito-idp.${cognitoRegion}.amazonaws.com/${userPoolId}/.well-known/openid-configuration`;
// Create execution role
this.role = new iam.Role(this, 'Role', {
assumedBy: new iam.ServicePrincipal('bedrock-agentcore.amazonaws.com'),
description: `Execution role for ${props.mcpName} MCP server`
});
// Add base CloudWatch Logs permissions
this.role.addToPolicy(new iam.PolicyStatement({
sid: 'CloudWatchLogs',
effect: iam.Effect.ALLOW,
actions: [
'logs:CreateLogGroup',
'logs:CreateLogStream',
'logs:PutLogEvents'
],
resources: [`arn:aws:logs:${this.region}:${this.account}:log-group:/aws/bedrock-agentcore/*`]
}));
// Add Bedrock model invocation permissions (including cross-region inference)
this.role.addToPolicy(new iam.PolicyStatement({
sid: 'BedrockModelInvoke',
effect: iam.Effect.ALLOW,
actions: [
'bedrock:InvokeModel',
'bedrock:InvokeModelWithResponseStream'
],
resources: [
`arn:aws:bedrock:*::foundation-model/*`,
`arn:aws:bedrock:*:${this.account}:inference-profile/*`
]
}));
// Add additional policies if provided
if (props.additionalPolicies) {
props.additionalPolicies.forEach(policy => this.role.addToPolicy(policy));
}
// Add SSM parameter read access if provided
if (props.ssmParameters && props.ssmParameters.length > 0) {
this.role.addToPolicy(new iam.PolicyStatement({
sid: 'SSMParameterAccess',
effect: iam.Effect.ALLOW,
actions: ['ssm:GetParameter', 'ssm:GetParameters'],
resources: props.ssmParameters.map(param =>
`arn:aws:ssm:${this.region}:${this.account}:parameter${param}`
)
}));
// Add KMS decrypt permission for SecureString parameters
this.role.addToPolicy(new iam.PolicyStatement({
sid: 'KMSDecrypt',
effect: iam.Effect.ALLOW,
actions: ['kms:Decrypt'],
resources: [`arn:aws:kms:${this.region}:${this.account}:key/*`],
conditions: {
StringEquals: {
'kms:ViaService': `ssm.${this.region}.amazonaws.com`
}
}
}));
}
// Build environment variables
const envVars = {
AWS_REGION: this.region,
...props.environmentVariables
};
// Create MCP Runtime with OAuth authentication
this.runtime = new agentcore.Runtime(this, 'Runtime', {
runtimeName: sanitizeName(`${props.mcpName}_mcp_${this.stackName}`),
agentRuntimeArtifact: agentcore.AgentRuntimeArtifact.fromAsset(
path.join(__dirname, '../../..', props.agentCodePath) // nosemgrep
),
executionRole: this.role,
protocolConfiguration: agentcore.ProtocolType.MCP,
networkConfiguration: agentcore.RuntimeNetworkConfiguration.usingPublicNetwork(),
authorizerConfiguration: agentcore.RuntimeAuthorizerConfiguration.usingOAuth(
cognitoDiscoveryUrl,
machineClientId
),
environmentVariables: envVars,
description: `MCP Server: ${props.mcpName} (OAuth enabled)`
});
// Outputs
new cdk.CfnOutput(this, 'RuntimeArn', {
value: this.runtime.agentRuntimeArn,
description: `${props.mcpName} MCP Runtime ARN`,
exportName: `${this.stackName}-RuntimeArn`
});
new cdk.CfnOutput(this, 'RuntimeId', {
value: this.runtime.agentRuntimeId,
description: `${props.mcpName} MCP Runtime ID`,
exportName: `${this.stackName}-RuntimeId`
});
}
}
@@ -0,0 +1,55 @@
import * as cdk from 'aws-cdk-lib';
import * as iam from 'aws-cdk-lib/aws-iam';
import { Construct } from 'constructs';
import { BaseMcpStack } from './base-mcp-stack';
import * as fs from 'fs';
// Load deployment config
const deploymentConfig = JSON.parse(fs.readFileSync('../../deployment-config.json', 'utf-8'));
const DEPLOYMENT_ID = deploymentConfig.deploymentId;
export class CartStack extends BaseMcpStack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, {
...props,
mcpName: 'cart',
agentCodePath: 'concierge_agent/mcp_cart_tools',
ssmParameters: [],
environmentVariables: {
USER_PROFILE_TABLE_NAME: cdk.Fn.importValue(`ConciergeAgent-${DEPLOYMENT_ID}-Data-UserProfileTableName`),
WISHLIST_TABLE_NAME: cdk.Fn.importValue(`ConciergeAgent-${DEPLOYMENT_ID}-Data-WishlistTableName`)
},
additionalPolicies: [
new iam.PolicyStatement({
sid: 'DynamoDBAccess',
effect: iam.Effect.ALLOW,
actions: [
'dynamodb:GetItem',
'dynamodb:PutItem',
'dynamodb:UpdateItem',
'dynamodb:DeleteItem',
'dynamodb:Query',
'dynamodb:Scan'
],
resources: [
`arn:aws:dynamodb:*:*:table/*`
]
})
]
});
// Add Visa secrets policy after super() so we can use 'this'
this.runtime.addToRolePolicy(
new iam.PolicyStatement({
sid: 'VisaSecretsAccess',
effect: iam.Effect.ALLOW,
actions: [
'secretsmanager:GetSecretValue'
],
resources: [
`arn:aws:secretsmanager:${this.region}:${this.account}:secret:visa/*`
]
})
);
}
}
@@ -0,0 +1,16 @@
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { BaseMcpStack } from './base-mcp-stack';
export class ShoppingStack extends BaseMcpStack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, {
...props,
mcpName: 'shopping',
agentCodePath: 'concierge_agent/mcp_shopping_tools',
ssmParameters: [
'/concierge-agent/shopping/serp-api-key'
]
});
}
}
@@ -0,0 +1,124 @@
import * as cdk from 'aws-cdk-lib';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as apigateway from 'aws-cdk-lib/aws-apigateway';
import * as iam from 'aws-cdk-lib/aws-iam';
import { Construct } from 'constructs';
export class VisaLambdaStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const deploymentId = this.node.tryGetContext('deploymentId') || 'default';
// Lambda function for Visa proxy - using standard Python runtime with ZIP deployment
const visaLambda = new lambda.Function(this, 'VisaProxyLambda', {
runtime: lambda.Runtime.PYTHON_3_11,
handler: 'handler.handler',
code: lambda.Code.fromAsset('../../concierge_agent/local-visa-server', {
bundling: {
image: lambda.Runtime.PYTHON_3_11.bundlingImage,
command: [
'bash', '-c',
[
// Install dependencies with correct platform binaries
'pip install -r requirements.txt --platform manylinux2014_x86_64 --only-binary=:all: --target /asset-output',
// Copy Python source files (exclude __pycache__)
'cp *.py /asset-output/',
// Copy visa module (exclude __pycache__)
'rsync -av --exclude="__pycache__" --exclude="*.pyc" --exclude="*.pyo" visa/ /asset-output/visa/',
// Copy config directory
'cp -r config /asset-output/',
].join(' && ')
],
},
}),
timeout: cdk.Duration.seconds(30),
memorySize: 512,
environment: {
CLIENT_APP_ID: 'VICTestAccountTR',
// AWS_REGION is automatically set by Lambda runtime
},
description: 'Visa Payment API Proxy Lambda',
});
// Grant Secrets Manager access for Visa credentials
visaLambda.addToRolePolicy(new iam.PolicyStatement({
actions: ['secretsmanager:GetSecretValue'],
resources: [
// Allow access to visa/* secrets (where actual Visa credentials are stored)
`arn:aws:secretsmanager:${this.region}:${this.account}:secret:visa/*`,
// Also allow access to deployment-specific secrets if they exist
`arn:aws:secretsmanager:${this.region}:${this.account}:secret:concierge-agent/${deploymentId}/visa/*`,
],
}));
// API Gateway REST API
const api = new apigateway.RestApi(this, 'VisaProxyApi', {
restApiName: 'Visa Proxy API',
description: 'Proxy for Visa Payment API calls',
deployOptions: {
stageName: 'prod',
tracingEnabled: true,
loggingLevel: apigateway.MethodLoggingLevel.INFO,
metricsEnabled: true,
},
defaultCorsPreflightOptions: {
allowOrigins: apigateway.Cors.ALL_ORIGINS, // Allow all origins for simplicity
allowMethods: ['GET', 'POST', 'OPTIONS'],
allowHeaders: ['Content-Type', 'Authorization', 'X-Amz-Date', 'X-Api-Key'],
allowCredentials: false,
},
});
// Add /api/visa/* proxy resource
const apiResource = api.root.addResource('api');
const visaResource = apiResource.addResource('visa');
// Add proxy+ to catch all paths under /api/visa/
visaResource.addProxy({
defaultIntegration: new apigateway.LambdaIntegration(visaLambda, {
timeout: cdk.Duration.seconds(29),
proxy: true,
}),
anyMethod: true,
});
// Add CORS headers to Gateway error responses (4XX, 5XX)
// This ensures CORS headers are present even when Lambda returns errors
const corsHeaders = {
'Access-Control-Allow-Origin': "'*'",
'Access-Control-Allow-Headers': "'Content-Type,Authorization'",
'Access-Control-Allow-Methods': "'GET,POST,OPTIONS'",
};
// Add CORS to all error response types
api.addGatewayResponse('Default4xx', {
type: apigateway.ResponseType.DEFAULT_4XX,
responseHeaders: corsHeaders,
});
api.addGatewayResponse('Default5xx', {
type: apigateway.ResponseType.DEFAULT_5XX,
responseHeaders: corsHeaders,
});
// CloudFormation Outputs
new cdk.CfnOutput(this, 'VisaProxyApiUrl', {
value: api.url,
description: 'Visa Proxy API Gateway URL (use this in frontend VITE_VISA_PROXY_URL)',
exportName: `VisaProxyApiUrl-${deploymentId}`,
});
new cdk.CfnOutput(this, 'VisaLambdaFunctionName', {
value: visaLambda.functionName,
description: 'Visa Lambda Function Name',
exportName: `VisaLambdaFunctionName-${deploymentId}`,
});
new cdk.CfnOutput(this, 'VisaLambdaArn', {
value: visaLambda.functionArn,
description: 'Visa Lambda Function ARN',
exportName: `VisaLambdaArn-${deploymentId}`,
});
}
}
@@ -0,0 +1,304 @@
import * as cdk from 'aws-cdk-lib';
import * as agentcore from '@aws-cdk/aws-bedrock-agentcore-alpha';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as cognito from 'aws-cdk-lib/aws-cognito';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as customResources from 'aws-cdk-lib/custom-resources';
import { Construct } from 'constructs';
import { GatewayConstruct } from './constructs/gateway-construct';
import * as path from 'path';
import * as fs from 'fs';
// Sanitize names for AgentCore resources (alphanumeric + underscores)
const sanitizeName = (name: string) =>
name.toLowerCase().replace(/[^a-z0-9]/g, '_').replace(/_+/g, '_');
// Load deployment config
const deploymentConfig = JSON.parse(fs.readFileSync('../../deployment-config.json', 'utf-8'));
const DEPLOYMENT_ID = deploymentConfig.deploymentId;
export class AgentStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// Add deployment info
cdk.Tags.of(this).add('DeploymentId', DEPLOYMENT_ID);
cdk.Tags.of(this).add('DeploymentName', deploymentConfig.deploymentName || DEPLOYMENT_ID);
// Import Cognito from Amplify
const userPoolId = cdk.Fn.importValue(`ConciergeAgent-${DEPLOYMENT_ID}-Auth-UserPoolId`);
const clientId = cdk.Fn.importValue(`ConciergeAgent-${DEPLOYMENT_ID}-Auth-ClientId`);
const cognitoRegion = cdk.Fn.importValue(`ConciergeAgent-${DEPLOYMENT_ID}-Auth-Region`);
const discoveryUrl = `https://cognito-idp.${cognitoRegion}.amazonaws.com/${userPoolId}/.well-known/openid-configuration`;
// Import machine client ID from Amplify (created there for proper deployment order)
const machineClientId = cdk.Fn.importValue(`ConciergeAgent-${DEPLOYMENT_ID}-Auth-MachineClientId`);
// Import the user pool for OAuth provider secret retrieval
const userPool = cognito.UserPool.fromUserPoolId(this, 'ImportedUserPool', userPoolId);
// Import DynamoDB tables
const userProfileTableName = cdk.Fn.importValue(`ConciergeAgent-${DEPLOYMENT_ID}-Data-UserProfileTableName`);
const wishlistTableName = cdk.Fn.importValue(`ConciergeAgent-${DEPLOYMENT_ID}-Data-WishlistTableName`);
const itineraryTableName = cdk.Fn.importValue(`ConciergeAgent-${DEPLOYMENT_ID}-Data-ItineraryTableName`);
const feedbackTableName = cdk.Fn.importValue(`ConciergeAgent-${DEPLOYMENT_ID}-Data-FeedbackTableName`);
// Import MCP runtime ARNs (using deployment ID in stack names)
const travelRuntimeArn = cdk.Fn.importValue(`TravelStack-${DEPLOYMENT_ID}-RuntimeArn`);
const cartRuntimeArn = cdk.Fn.importValue(`CartStack-${DEPLOYMENT_ID}-RuntimeArn`);
const itineraryRuntimeArn = cdk.Fn.importValue(`ItineraryStack-${DEPLOYMENT_ID}-RuntimeArn`);
// 1. Create Custom Resource Lambda for OAuth Provider
const oauthProviderRole = new iam.Role(this, 'OAuthProviderLambdaRole', {
assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
description: 'Execution role for OAuth Provider Custom Resource Lambda',
managedPolicies: [
iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole')
],
inlinePolicies: {
OAuthProviderPolicy: new iam.PolicyDocument({
statements: [
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: [
'bedrock-agentcore:CreateOAuth2CredentialProvider',
'bedrock-agentcore:DeleteOAuth2CredentialProvider',
'bedrock-agentcore:ListOAuth2CredentialProviders',
'bedrock-agentcore:GetOAuth2CredentialProvider',
'bedrock-agentcore:CreateTokenVault', // Required for OAuth provider creation
'bedrock-agentcore:DeleteTokenVault',
'bedrock-agentcore:GetTokenVault',
'secretsmanager:CreateSecret', // Required to store OAuth client secret
'secretsmanager:DeleteSecret',
'secretsmanager:GetSecretValue',
'secretsmanager:PutSecretValue',
'cognito-idp:DescribeUserPoolClient' // Required to fetch client secret
],
resources: ['*']
})
]
})
}
});
const oauthProviderLambda = new lambda.Function(this, 'OAuthProviderLambda', {
runtime: lambda.Runtime.PYTHON_3_13,
handler: 'index.handler',
code: lambda.Code.fromAsset(path.join(__dirname, '..', 'lambdas', 'oauth-provider')),
timeout: cdk.Duration.minutes(5),
role: oauthProviderRole,
description: 'Custom Resource for OAuth2 Credential Provider management'
});
const oauthProvider = new customResources.Provider(this, 'OAuthProvider', {
onEventHandler: oauthProviderLambda
});
// 2. Create OAuth Provider via Custom Resource
const oauthCredentialProvider = new cdk.CustomResource(this, 'OAuthCredentialProvider', {
serviceToken: oauthProvider.serviceToken,
properties: {
ProviderName: sanitizeName(`oauth_provider_${this.stackName}`),
UserPoolId: userPoolId,
ClientId: machineClientId,
DiscoveryUrl: discoveryUrl,
Version: '2' // Increment to force update
}
});
const oauthProviderArn = oauthCredentialProvider.getAttString('ProviderArn');
// 1. Create Memory using L2 construct
const memory = new agentcore.Memory(this, 'Memory', {
memoryName: sanitizeName(`memory_${this.stackName}`),
description: 'Short-term memory for Concierge Agent',
});
// 2. Create execution role for main agent
const agentRole = new iam.Role(this, 'AgentRole', {
assumedBy: new iam.ServicePrincipal('bedrock-agentcore.amazonaws.com'),
description: 'Execution role for Concierge Agent'
});
// Grant DynamoDB permissions
agentRole.addToPolicy(new iam.PolicyStatement({
actions: ['dynamodb:GetItem', 'dynamodb:Scan', 'dynamodb:UpdateItem', 'dynamodb:Query', 'dynamodb:PutItem', 'dynamodb:DeleteItem', 'dynamodb:BatchWriteItem'],
resources: [
`arn:aws:dynamodb:${this.region}:${this.account}:table/${userProfileTableName}`,
`arn:aws:dynamodb:${this.region}:${this.account}:table/${userProfileTableName}/index/*`,
`arn:aws:dynamodb:${this.region}:${this.account}:table/${wishlistTableName}`,
`arn:aws:dynamodb:${this.region}:${this.account}:table/${wishlistTableName}/index/*`,
`arn:aws:dynamodb:${this.region}:${this.account}:table/${itineraryTableName}`,
`arn:aws:dynamodb:${this.region}:${this.account}:table/${itineraryTableName}/index/*`,
`arn:aws:dynamodb:${this.region}:${this.account}:table/${feedbackTableName}`,
`arn:aws:dynamodb:${this.region}:${this.account}:table/${feedbackTableName}/index/*`
]
}));
// Grant CloudWatch Logs permissions
agentRole.addToPolicy(new iam.PolicyStatement({
actions: ['logs:CreateLogGroup', 'logs:CreateLogStream', 'logs:PutLogEvents', 'logs:DescribeLogGroups', 'logs:DescribeLogStreams', 'logs:GetLogEvents', 'logs:FilterLogEvents'],
resources: [`arn:aws:logs:${this.region}:${this.account}:log-group:*`]
}));
// Grant Memory access
agentRole.addToPolicy(new iam.PolicyStatement({
actions: [
'bedrock-agentcore:GetMemory',
'bedrock-agentcore:ListMemories',
'bedrock-agentcore:CreateEvent',
'bedrock-agentcore:GetEvent',
'bedrock-agentcore:ListEvents',
'bedrock-agentcore:RetrieveMemoryRecords'
],
resources: [memory.memoryArn]
}));
// Grant Bedrock model access
agentRole.addToPolicy(new iam.PolicyStatement({
actions: ['bedrock:InvokeModel', 'bedrock:InvokeModelWithResponseStream'],
resources: [
`arn:aws:bedrock:*::foundation-model/*`,
`arn:aws:bedrock:*:${this.account}:inference-profile/*`
]
}));
// Grant Gateway invoke permissions (for runtime to call gateway)
agentRole.addToPolicy(new iam.PolicyStatement({
actions: ['bedrock-agentcore:InvokeGateway'],
resources: ['*'] // Will be restricted to specific gateway after creation
}));
// Grant ECR permissions (required for runtime to pull container image)
agentRole.addToPolicy(new iam.PolicyStatement({
actions: [
'ecr:GetAuthorizationToken',
'ecr:BatchCheckLayerAvailability',
'ecr:GetDownloadUrlForLayer',
'ecr:BatchGetImage'
],
resources: ['*']
}));
// Grant Cognito permissions (required for runtime to fetch machine client secret and domain)
agentRole.addToPolicy(new iam.PolicyStatement({
actions: [
'cognito-idp:DescribeUserPoolClient',
'cognito-idp:DescribeUserPool'
],
resources: [`arn:aws:cognito-idp:${this.region}:${this.account}:userpool/${userPoolId}`]
}));
// 3. Create main agent runtime using L2 construct
const baseEnvVars = {
MEMORY_ID: memory.memoryId,
USER_PROFILE_TABLE_NAME: userProfileTableName,
WISHLIST_TABLE_NAME: wishlistTableName,
ITINERARY_TABLE_NAME: itineraryTableName,
FEEDBACK_TABLE_NAME: feedbackTableName,
DEPLOYMENT_ID: DEPLOYMENT_ID,
// Gateway M2M authentication credentials (for runtime to call gateway)
GATEWAY_CLIENT_ID: machineClientId,
GATEWAY_USER_POOL_ID: userPoolId,
GATEWAY_SCOPE: 'concierge-gateway/invoke',
};
// Create runtime first (without GATEWAY_URL since gateway doesn't exist yet)
const runtime = new agentcore.Runtime(this, 'Runtime', {
runtimeName: sanitizeName(`agent_${this.stackName}`),
agentRuntimeArtifact: agentcore.AgentRuntimeArtifact.fromAsset(
path.join(__dirname, '../../..', 'concierge_agent', 'supervisor_agent')
),
executionRole: agentRole,
protocolConfiguration: agentcore.ProtocolType.HTTP,
networkConfiguration: agentcore.RuntimeNetworkConfiguration.usingPublicNetwork(),
authorizerConfiguration: agentcore.RuntimeAuthorizerConfiguration.usingJWT(
discoveryUrl,
[clientId, machineClientId] // Accept user tokens (frontend) AND M2M tokens (gateway)
),
environmentVariables: baseEnvVars,
description: 'Concierge Agent Runtime'
});
// 6. Create Gateway with JWT inbound and OAuth outbound
// IMPORTANT: Gateway must use SAME M2M client as runtime and OAuth provider
// Inbound: Frontend authenticates to gateway using M2M JWT token
// Outbound: Gateway authenticates to targets using OAuth (same M2M client)
const gateway = new GatewayConstruct(this, 'Gateway', {
gatewayName: sanitizeName(`gateway_${this.stackName}`).replace(/_/g, '-'),
mcpRuntimeArns: [
{ name: 'CartTools', arn: cartRuntimeArn },
{ name: 'ItineraryTools', arn: itineraryRuntimeArn },
{ name: 'TravelTools', arn: travelRuntimeArn }
],
cognitoClientId: machineClientId, // Use M2M client (same as runtime and OAuth)
cognitoDiscoveryUrl: discoveryUrl,
oauthProviderArn: oauthProviderArn,
oauthScope: 'concierge-gateway/invoke'
});
// Store gateway URL in SSM Parameter Store for runtime to access
const gatewayUrlParameter = new cdk.aws_ssm.StringParameter(this, 'GatewayUrlParameter', {
parameterName: `/concierge-agent/${DEPLOYMENT_ID}/gateway-url`,
stringValue: gateway.gatewayUrl,
description: 'AgentCore Gateway URL for supervisor agent',
tier: cdk.aws_ssm.ParameterTier.STANDARD,
});
// Grant runtime permission to read SSM parameter
agentRole.addToPolicy(new iam.PolicyStatement({
actions: ['ssm:GetParameter'],
resources: [gatewayUrlParameter.parameterArn]
}));
// Outputs
new cdk.CfnOutput(this, 'MainRuntimeArn', {
value: runtime.agentRuntimeArn,
exportName: `${this.stackName}-MainRuntimeArn`
});
new cdk.CfnOutput(this, 'MainRuntimeId', {
value: runtime.agentRuntimeId,
exportName: `${this.stackName}-MainRuntimeId`
});
new cdk.CfnOutput(this, 'MemoryId', {
value: memory.memoryId,
exportName: `${this.stackName}-MemoryId`
});
new cdk.CfnOutput(this, 'GatewayUrl', {
value: gateway.gatewayUrl,
exportName: `${this.stackName}-GatewayUrl`,
description: 'Gateway URL for MCP client connections (IAM auth)'
});
new cdk.CfnOutput(this, 'GatewayId', {
value: gateway.gatewayId,
exportName: `${this.stackName}-GatewayId`,
description: 'Gateway ID'
});
new cdk.CfnOutput(this, 'GatewayArn', {
value: gateway.gatewayArn,
exportName: `${this.stackName}-GatewayArn`,
description: 'Gateway ARN'
});
new cdk.CfnOutput(this, 'GatewayTargetCount', {
value: gateway.targets.length.toString(),
description: 'Number of gateway targets configured'
});
new cdk.CfnOutput(this, 'MachineClientId', {
value: machineClientId,
description: 'Machine client ID for gateway OAuth and MCP authentication (imported from Amplify)'
});
new cdk.CfnOutput(this, 'OAuthProviderArn', {
value: oauthProviderArn,
exportName: `${this.stackName}-OAuthProviderArn`,
description: 'OAuth provider ARN for gateway targets'
});
}
}
@@ -0,0 +1,21 @@
#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import * as fs from 'fs';
import { AgentStack } from './agent-stack';
// Load deployment config
const deploymentConfig = JSON.parse(fs.readFileSync('../../deployment-config.json', 'utf-8'));
const DEPLOYMENT_ID = deploymentConfig.deploymentId;
const app = new cdk.App();
new AgentStack(app, `AgentStack-${DEPLOYMENT_ID}`, {
env: {
account: process.env.CDK_DEFAULT_ACCOUNT,
region: process.env.CDK_DEFAULT_REGION || 'us-east-1'
},
description: `Concierge Agent with Gateway - Main agent runtime, memory, and gateway with MCP targets (${DEPLOYMENT_ID})`
});
app.synth();
@@ -0,0 +1,172 @@
import * as cdk from 'aws-cdk-lib';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as bedrockagentcore from 'aws-cdk-lib/aws-bedrockagentcore';
import { Construct } from 'constructs';
export interface GatewayProps {
gatewayName: string;
mcpRuntimeArns: { name: string; arn: string }[];
cognitoClientId: string;
cognitoDiscoveryUrl: string;
oauthProviderArn: string;
oauthScope: string;
}
export class GatewayConstruct extends Construct {
public readonly gateway: bedrockagentcore.CfnGateway;
public readonly gatewayArn: string;
public readonly gatewayId: string;
public readonly gatewayUrl: string;
public readonly targets: bedrockagentcore.CfnGatewayTarget[];
constructor(scope: Construct, id: string, props: GatewayProps) {
super(scope, id);
const stack = cdk.Stack.of(this);
// Gateway Role with permissions to invoke runtimes and Lambda functions
const gatewayRole = new iam.Role(this, 'GatewayRole', {
assumedBy: new iam.ServicePrincipal('bedrock-agentcore.amazonaws.com'),
description: 'Execution role for AgentCore Gateway',
inlinePolicies: {
GatewayPolicy: new iam.PolicyDocument({
statements: [
// Bedrock AgentCore permissions (only if there are MCP runtimes)
...(props.mcpRuntimeArns.length > 0 ? [new iam.PolicyStatement({
sid: 'BedrockAgentCoreAccess',
effect: iam.Effect.ALLOW,
actions: [
'bedrock-agentcore:InvokeRuntime',
'bedrock-agentcore:InvokeRuntimeWithResponseStream'
],
resources: [
...props.mcpRuntimeArns.map(r => r.arn)
],
})] : []),
// Lambda invoke permissions (for future Lambda targets)
new iam.PolicyStatement({
sid: 'LambdaInvokeAccess',
effect: iam.Effect.ALLOW,
actions: ['lambda:InvokeFunction'],
resources: [`arn:aws:lambda:${stack.region}:${stack.account}:function:*`],
}),
// Bedrock model access (for gateway operations)
new iam.PolicyStatement({
sid: 'BedrockModelAccess',
effect: iam.Effect.ALLOW,
actions: ['bedrock:InvokeModel', 'bedrock:InvokeModelWithResponseStream'],
resources: [
`arn:aws:bedrock:*::foundation-model/*`,
`arn:aws:bedrock:*:${stack.account}:inference-profile/*`
],
}),
// CloudWatch Logs
new iam.PolicyStatement({
sid: 'CloudWatchLogsAccess',
effect: iam.Effect.ALLOW,
actions: [
'logs:CreateLogGroup',
'logs:CreateLogStream',
'logs:PutLogEvents'
],
resources: [`arn:aws:logs:${stack.region}:${stack.account}:log-group:/aws/bedrock-agentcore/*`],
}),
// OAuth Provider access (for authenticating to MCP servers)
new iam.PolicyStatement({
sid: 'OAuthProviderAccess',
effect: iam.Effect.ALLOW,
actions: [
'bedrock-agentcore:GetOAuth2CredentialProvider',
'bedrock-agentcore:GetTokenVault',
'bedrock-agentcore:GetWorkloadAccessToken', // Required for OAuth workload identity
'bedrock-agentcore:GetResourceOauth2Token', // Required to get OAuth token from provider
'secretsmanager:GetSecretValue'
],
resources: [
props.oauthProviderArn,
`arn:aws:secretsmanager:${stack.region}:${stack.account}:secret:*`,
`arn:aws:bedrock-agentcore:${stack.region}:${stack.account}:workload-identity-directory/*`,
`arn:aws:bedrock-agentcore:${stack.region}:${stack.account}:token-vault/*`
],
}),
],
}),
},
});
// Create Gateway with JWT inbound auth
this.gateway = new bedrockagentcore.CfnGateway(this, 'Gateway', {
name: props.gatewayName,
roleArn: gatewayRole.roleArn,
protocolType: 'MCP',
protocolConfiguration: {
mcp: {
supportedVersions: ['2025-03-26']
}
},
authorizerType: 'CUSTOM_JWT',
authorizerConfiguration: {
customJwtAuthorizer: {
allowedClients: [props.cognitoClientId],
discoveryUrl: props.cognitoDiscoveryUrl
}
},
description: 'AgentCore Gateway with MCP protocol, JWT inbound auth, and OAuth outbound auth'
});
this.gatewayArn = this.gateway.attrGatewayArn;
this.gatewayId = this.gateway.attrGatewayIdentifier;
this.gatewayUrl = this.gateway.attrGatewayUrl;
// Create targets for MCP servers only
// Note: Main runtime calls the gateway, not the other way around
this.targets = [];
// MCP server targets with OAuth credentials
props.mcpRuntimeArns.forEach((mcpRuntime, index) => {
const mcpRuntimeUrl = this.constructRuntimeUrl(mcpRuntime.arn, stack.region);
const mcpTarget = new bedrockagentcore.CfnGatewayTarget(this, `McpTarget${index}`, {
gatewayIdentifier: this.gatewayId,
name: mcpRuntime.name.toLowerCase().replace(/[^a-z0-9-]/g, '-'),
description: `MCP Server: ${mcpRuntime.name}`,
targetConfiguration: {
mcp: {
mcpServer: {
endpoint: mcpRuntimeUrl
}
}
},
credentialProviderConfigurations: [{
credentialProviderType: 'OAUTH',
credentialProvider: {
oauthCredentialProvider: {
providerArn: props.oauthProviderArn,
scopes: [props.oauthScope]
}
}
}]
});
mcpTarget.addDependency(this.gateway);
this.targets.push(mcpTarget);
});
// Note: Outputs are created in the parent stack to avoid duplicate exports
}
/**
* Constructs the runtime invocation URL with properly encoded ARN
* Format: https://bedrock-agentcore.{region}.amazonaws.com/runtimes/{encoded-arn}/invocations
*
* Note: ARN encoding is handled by CloudFormation Fn::Join with URL-encoded characters
*/
private constructRuntimeUrl(runtimeArn: string, region: string): string {
// Use CloudFormation Fn::Join to construct URL with encoded ARN
// The ARN needs to be URL-encoded: : becomes %3A and / becomes %2F
return cdk.Fn.join('', [
`https://bedrock-agentcore.${region}.amazonaws.com/runtimes/`,
// Split ARN by : and / and rejoin with encoded versions
cdk.Fn.join('%2F', cdk.Fn.split('/', cdk.Fn.join('%3A', cdk.Fn.split(':', runtimeArn)))),
'/invocations'
]);
}
}
@@ -0,0 +1,21 @@
#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import * as fs from 'fs';
import { FrontendStack } from './frontend-stack';
// Load deployment config
const deploymentConfig = JSON.parse(fs.readFileSync('../../deployment-config.json', 'utf-8'));
const DEPLOYMENT_ID = deploymentConfig.deploymentId;
const app = new cdk.App();
new FrontendStack(app, `FrontendStack-${DEPLOYMENT_ID}`, {
env: {
account: process.env.CDK_DEFAULT_ACCOUNT,
region: process.env.CDK_DEFAULT_REGION || 'us-east-1'
},
description: `Concierge Agent Frontend - Amplify Hosting (${DEPLOYMENT_ID})`
});
app.synth();
@@ -0,0 +1,85 @@
import * as cdk from 'aws-cdk-lib';
import * as amplify from '@aws-cdk/aws-amplify-alpha';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as iam from 'aws-cdk-lib/aws-iam';
import { Construct } from 'constructs';
import * as fs from 'fs';
// Load deployment config
const deploymentConfig = JSON.parse(fs.readFileSync('../../deployment-config.json', 'utf-8'));
const DEPLOYMENT_ID = deploymentConfig.deploymentId;
export class FrontendStack extends cdk.Stack {
public readonly amplifyApp: amplify.App;
public readonly amplifyUrl: string;
public readonly stagingBucket: s3.Bucket;
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// Add deployment info
cdk.Tags.of(this).add('DeploymentId', DEPLOYMENT_ID);
cdk.Tags.of(this).add('DeploymentName', deploymentConfig.deploymentName || DEPLOYMENT_ID);
// Create staging bucket for Amplify deployments
this.stagingBucket = new s3.Bucket(this, 'StagingBucket', {
removalPolicy: cdk.RemovalPolicy.DESTROY,
autoDeleteObjects: true,
versioned: true,
publicReadAccess: false,
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
lifecycleRules: [
{
id: 'DeleteOldDeployments',
enabled: true,
expiration: cdk.Duration.days(30),
},
],
});
// Add bucket policy for Amplify access
this.stagingBucket.addToResourcePolicy(
new iam.PolicyStatement({
sid: 'AmplifyAccess',
effect: iam.Effect.ALLOW,
principals: [new iam.ServicePrincipal('amplify.amazonaws.com')],
actions: ['s3:GetObject', 's3:GetObjectVersion'],
resources: [this.stagingBucket.arnForObjects('*')],
})
);
// Create Amplify app
this.amplifyApp = new amplify.App(this, 'AmplifyApp', {
appName: 'concierge-agent-frontend',
description: 'Concierge Agent - React/Vite Frontend',
platform: amplify.Platform.WEB,
});
// Create main branch
this.amplifyApp.addBranch('main', {
stage: 'PRODUCTION',
branchName: 'main',
});
// Predictable domain format
this.amplifyUrl = `https://main.${this.amplifyApp.appId}.amplifyapp.com`;
// Outputs
new cdk.CfnOutput(this, 'AmplifyAppId', {
value: this.amplifyApp.appId,
description: 'Amplify App ID',
exportName: `${this.stackName}-AmplifyAppId`
});
new cdk.CfnOutput(this, 'AmplifyUrl', {
value: this.amplifyUrl,
description: 'Amplify App URL'
});
new cdk.CfnOutput(this, 'StagingBucketName', {
value: this.stagingBucket.bucketName,
description: 'S3 Staging Bucket Name',
exportName: `${this.stackName}-StagingBucketName`
});
}
}
@@ -0,0 +1,35 @@
#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import * as fs from 'fs';
import { TravelStack } from './travel-stack';
import { CartStack } from './cart-stack';
import { ItineraryStack } from './itinerary-stack';
// Load deployment config
const deploymentConfig = JSON.parse(fs.readFileSync('../../deployment-config.json', 'utf-8'));
const DEPLOYMENT_ID = deploymentConfig.deploymentId;
const app = new cdk.App();
const env = {
account: process.env.CDK_DEFAULT_ACCOUNT,
region: process.env.CDK_DEFAULT_REGION || 'us-east-1'
};
new TravelStack(app, `TravelStack-${DEPLOYMENT_ID}`, {
env,
description: `Travel MCP Server - Travel search tools (${DEPLOYMENT_ID})`
});
new CartStack(app, `CartStack-${DEPLOYMENT_ID}`, {
env,
description: `Cart MCP Server - Shopping cart tools (${DEPLOYMENT_ID})`
});
new ItineraryStack(app, `ItineraryStack-${DEPLOYMENT_ID}`, {
env,
description: `Itinerary MCP Server - Itinerary management tools (${DEPLOYMENT_ID})`
});
app.synth();
@@ -0,0 +1,139 @@
import * as cdk from 'aws-cdk-lib';
import * as agentcore from '@aws-cdk/aws-bedrock-agentcore-alpha';
import * as iam from 'aws-cdk-lib/aws-iam';
import { Construct } from 'constructs';
import * as path from 'path';
import * as fs from 'fs';
// Sanitize names for AgentCore resources (alphanumeric + underscores only)
const sanitizeName = (name: string) =>
name.toLowerCase().replace(/[^a-z0-9]/g, '_').replace(/_+/g, '_');
// Load deployment config
const deploymentConfig = JSON.parse(fs.readFileSync('../../deployment-config.json', 'utf-8'));
const DEPLOYMENT_ID = deploymentConfig.deploymentId;
export interface BaseMcpStackProps extends cdk.StackProps {
mcpName: string;
agentCodePath: string;
environmentVariables?: { [key: string]: string };
additionalPolicies?: iam.PolicyStatement[];
ssmParameters?: string[];
}
export abstract class BaseMcpStack extends cdk.Stack {
public readonly runtime: agentcore.Runtime;
public readonly role: iam.Role;
constructor(scope: Construct, id: string, props: BaseMcpStackProps) {
super(scope, id, props);
// Add deployment info
cdk.Tags.of(this).add('DeploymentId', DEPLOYMENT_ID);
cdk.Tags.of(this).add('DeploymentName', deploymentConfig.deploymentName || DEPLOYMENT_ID);
// Import Cognito configuration from Amplify stack
const userPoolId = cdk.Fn.importValue(`ConciergeAgent-${DEPLOYMENT_ID}-Auth-UserPoolId`);
const cognitoRegion = cdk.Fn.importValue(`ConciergeAgent-${DEPLOYMENT_ID}-Auth-Region`);
const machineClientId = cdk.Fn.importValue(`ConciergeAgent-${DEPLOYMENT_ID}-Auth-MachineClientId`);
const cognitoDiscoveryUrl = `https://cognito-idp.${cognitoRegion}.amazonaws.com/${userPoolId}/.well-known/openid-configuration`;
// Create execution role
this.role = new iam.Role(this, 'Role', {
assumedBy: new iam.ServicePrincipal('bedrock-agentcore.amazonaws.com'),
description: `Execution role for ${props.mcpName} MCP server`
});
// Add base CloudWatch Logs permissions
this.role.addToPolicy(new iam.PolicyStatement({
sid: 'CloudWatchLogs',
effect: iam.Effect.ALLOW,
actions: [
'logs:CreateLogGroup',
'logs:CreateLogStream',
'logs:PutLogEvents'
],
resources: [`arn:aws:logs:${this.region}:${this.account}:log-group:/aws/bedrock-agentcore/*`]
}));
// Add Bedrock model invocation permissions (including cross-region inference)
this.role.addToPolicy(new iam.PolicyStatement({
sid: 'BedrockModelInvoke',
effect: iam.Effect.ALLOW,
actions: [
'bedrock:InvokeModel',
'bedrock:InvokeModelWithResponseStream'
],
resources: [
`arn:aws:bedrock:*::foundation-model/*`,
`arn:aws:bedrock:*:${this.account}:inference-profile/*`
]
}));
// Add additional policies if provided
if (props.additionalPolicies) {
props.additionalPolicies.forEach(policy => this.role.addToPolicy(policy));
}
// Add SSM parameter read access if provided
if (props.ssmParameters && props.ssmParameters.length > 0) {
this.role.addToPolicy(new iam.PolicyStatement({
sid: 'SSMParameterAccess',
effect: iam.Effect.ALLOW,
actions: ['ssm:GetParameter', 'ssm:GetParameters'],
resources: props.ssmParameters.map(param =>
`arn:aws:ssm:${this.region}:${this.account}:parameter${param}`
)
}));
// Add KMS decrypt permission for SecureString parameters
this.role.addToPolicy(new iam.PolicyStatement({
sid: 'KMSDecrypt',
effect: iam.Effect.ALLOW,
actions: ['kms:Decrypt'],
resources: [`arn:aws:kms:${this.region}:${this.account}:key/*`],
conditions: {
StringEquals: {
'kms:ViaService': `ssm.${this.region}.amazonaws.com`
}
}
}));
}
// Build environment variables
const envVars = {
AWS_REGION: this.region,
...props.environmentVariables
};
// Create MCP Runtime with OAuth authentication
this.runtime = new agentcore.Runtime(this, 'Runtime', {
runtimeName: sanitizeName(`${props.mcpName}_mcp_${this.stackName}`),
agentRuntimeArtifact: agentcore.AgentRuntimeArtifact.fromAsset(
path.join(__dirname, '../../..', props.agentCodePath) // nosemgrep
),
executionRole: this.role,
protocolConfiguration: agentcore.ProtocolType.MCP,
networkConfiguration: agentcore.RuntimeNetworkConfiguration.usingPublicNetwork(),
authorizerConfiguration: agentcore.RuntimeAuthorizerConfiguration.usingOAuth(
cognitoDiscoveryUrl,
machineClientId
),
environmentVariables: envVars,
description: `MCP Server: ${props.mcpName} (OAuth enabled)`
});
// Outputs
new cdk.CfnOutput(this, 'RuntimeArn', {
value: this.runtime.agentRuntimeArn,
description: `${props.mcpName} MCP Runtime ARN`,
exportName: `${this.stackName}-RuntimeArn`
});
new cdk.CfnOutput(this, 'RuntimeId', {
value: this.runtime.agentRuntimeId,
description: `${props.mcpName} MCP Runtime ID`,
exportName: `${this.stackName}-RuntimeId`
});
}
}
@@ -0,0 +1,55 @@
import * as cdk from 'aws-cdk-lib';
import * as iam from 'aws-cdk-lib/aws-iam';
import { Construct } from 'constructs';
import { BaseMcpStack } from './base-mcp-stack';
import * as fs from 'fs';
// Load deployment config
const deploymentConfig = JSON.parse(fs.readFileSync('../../deployment-config.json', 'utf-8'));
const DEPLOYMENT_ID = deploymentConfig.deploymentId;
export class CartStack extends BaseMcpStack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, {
...props,
mcpName: 'cart',
agentCodePath: 'concierge_agent/mcp_cart_tools',
ssmParameters: [],
environmentVariables: {
USER_PROFILE_TABLE_NAME: cdk.Fn.importValue(`ConciergeAgent-${DEPLOYMENT_ID}-Data-UserProfileTableName`),
WISHLIST_TABLE_NAME: cdk.Fn.importValue(`ConciergeAgent-${DEPLOYMENT_ID}-Data-WishlistTableName`)
},
additionalPolicies: [
new iam.PolicyStatement({
sid: 'DynamoDBAccess',
effect: iam.Effect.ALLOW,
actions: [
'dynamodb:GetItem',
'dynamodb:PutItem',
'dynamodb:UpdateItem',
'dynamodb:DeleteItem',
'dynamodb:Query',
'dynamodb:Scan'
],
resources: [
`arn:aws:dynamodb:*:*:table/*`
]
})
]
});
// Add Visa secrets policy after super() so we can use 'this'
this.runtime.addToRolePolicy(
new iam.PolicyStatement({
sid: 'VisaSecretsAccess',
effect: iam.Effect.ALLOW,
actions: [
'secretsmanager:GetSecretValue'
],
resources: [
`arn:aws:secretsmanager:${this.region}:${this.account}:secret:visa/*`
]
})
);
}
}
@@ -0,0 +1,41 @@
import * as cdk from 'aws-cdk-lib';
import * as iam from 'aws-cdk-lib/aws-iam';
import { Construct } from 'constructs';
import { BaseMcpStack } from './base-mcp-stack';
import * as fs from 'fs';
// Load deployment config
const deploymentConfig = JSON.parse(fs.readFileSync('../../deployment-config.json', 'utf-8'));
const DEPLOYMENT_ID = deploymentConfig.deploymentId;
export class ItineraryStack extends BaseMcpStack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, {
...props,
mcpName: 'itinerary',
agentCodePath: 'concierge_agent/mcp_itinerary_tools',
ssmParameters: [],
environmentVariables: {
USER_PROFILE_TABLE_NAME: cdk.Fn.importValue(`ConciergeAgent-${DEPLOYMENT_ID}-Data-UserProfileTableName`),
ITINERARY_TABLE_NAME: cdk.Fn.importValue(`ConciergeAgent-${DEPLOYMENT_ID}-Data-ItineraryTableName`)
},
additionalPolicies: [
new iam.PolicyStatement({
sid: 'DynamoDBAccess',
effect: iam.Effect.ALLOW,
actions: [
'dynamodb:GetItem',
'dynamodb:PutItem',
'dynamodb:UpdateItem',
'dynamodb:DeleteItem',
'dynamodb:Query',
'dynamodb:Scan'
],
resources: [
`arn:aws:dynamodb:*:*:table/*`
]
})
]
});
}
}
@@ -0,0 +1,21 @@
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { BaseMcpStack } from './base-mcp-stack';
export class TravelStack extends BaseMcpStack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, {
...props,
mcpName: 'travel',
agentCodePath: 'concierge_agent/mcp_travel_tools',
ssmParameters: [
'/concierge-agent/travel/openweather-api-key',
'/concierge-agent/travel/tavily-api-key',
'/concierge-agent/travel/serp-api-key',
'/concierge-agent/travel/google-maps-key',
'/concierge-agent/travel/amadeus-public',
'/concierge-agent/travel/amadeus-secret'
]
});
}
}
@@ -0,0 +1,124 @@
import * as cdk from 'aws-cdk-lib';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as apigateway from 'aws-cdk-lib/aws-apigateway';
import * as iam from 'aws-cdk-lib/aws-iam';
import { Construct } from 'constructs';
export class VisaLambdaStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const deploymentId = this.node.tryGetContext('deploymentId') || 'default';
// Lambda function for Visa proxy - using standard Python runtime with ZIP deployment
const visaLambda = new lambda.Function(this, 'VisaProxyLambda', {
runtime: lambda.Runtime.PYTHON_3_11,
handler: 'handler.handler',
code: lambda.Code.fromAsset('../../concierge_agent/local-visa-server', {
bundling: {
image: lambda.Runtime.PYTHON_3_11.bundlingImage,
command: [
'bash', '-c',
[
// Install dependencies with correct platform binaries
'pip install -r requirements.txt --platform manylinux2014_x86_64 --only-binary=:all: --target /asset-output',
// Copy Python source files (exclude __pycache__)
'cp *.py /asset-output/',
// Copy visa module (exclude __pycache__)
'rsync -av --exclude="__pycache__" --exclude="*.pyc" --exclude="*.pyo" visa/ /asset-output/visa/',
// Copy config directory
'cp -r config /asset-output/',
].join(' && ')
],
},
}),
timeout: cdk.Duration.seconds(30),
memorySize: 512,
environment: {
CLIENT_APP_ID: 'VICTestAccountTR',
// AWS_REGION is automatically set by Lambda runtime
},
description: 'Visa Payment API Proxy Lambda',
});
// Grant Secrets Manager access for Visa credentials
visaLambda.addToRolePolicy(new iam.PolicyStatement({
actions: ['secretsmanager:GetSecretValue'],
resources: [
// Allow access to visa/* secrets (where actual Visa credentials are stored)
`arn:aws:secretsmanager:${this.region}:${this.account}:secret:visa/*`,
// Also allow access to deployment-specific secrets if they exist
`arn:aws:secretsmanager:${this.region}:${this.account}:secret:concierge-agent/${deploymentId}/visa/*`,
],
}));
// API Gateway REST API
const api = new apigateway.RestApi(this, 'VisaProxyApi', {
restApiName: 'Visa Proxy API',
description: 'Proxy for Visa Payment API calls',
deployOptions: {
stageName: 'prod',
tracingEnabled: true,
loggingLevel: apigateway.MethodLoggingLevel.INFO,
metricsEnabled: true,
},
defaultCorsPreflightOptions: {
allowOrigins: apigateway.Cors.ALL_ORIGINS, // Allow all origins for simplicity
allowMethods: ['GET', 'POST', 'OPTIONS'],
allowHeaders: ['Content-Type', 'Authorization', 'X-Amz-Date', 'X-Api-Key'],
allowCredentials: false,
},
});
// Add /api/visa/* proxy resource
const apiResource = api.root.addResource('api');
const visaResource = apiResource.addResource('visa');
// Add proxy+ to catch all paths under /api/visa/
visaResource.addProxy({
defaultIntegration: new apigateway.LambdaIntegration(visaLambda, {
timeout: cdk.Duration.seconds(29),
proxy: true,
}),
anyMethod: true,
});
// Add CORS headers to Gateway error responses (4XX, 5XX)
// This ensures CORS headers are present even when Lambda returns errors
const corsHeaders = {
'Access-Control-Allow-Origin': "'*'",
'Access-Control-Allow-Headers': "'Content-Type,Authorization'",
'Access-Control-Allow-Methods': "'GET,POST,OPTIONS'",
};
// Add CORS to all error response types
api.addGatewayResponse('Default4xx', {
type: apigateway.ResponseType.DEFAULT_4XX,
responseHeaders: corsHeaders,
});
api.addGatewayResponse('Default5xx', {
type: apigateway.ResponseType.DEFAULT_5XX,
responseHeaders: corsHeaders,
});
// CloudFormation Outputs
new cdk.CfnOutput(this, 'VisaProxyApiUrl', {
value: api.url,
description: 'Visa Proxy API Gateway URL (use this in frontend VITE_VISA_PROXY_URL)',
exportName: `VisaProxyApiUrl-${deploymentId}`,
});
new cdk.CfnOutput(this, 'VisaLambdaFunctionName', {
value: visaLambda.functionName,
description: 'Visa Lambda Function Name',
exportName: `VisaLambdaFunctionName-${deploymentId}`,
});
new cdk.CfnOutput(this, 'VisaLambdaArn', {
value: visaLambda.functionArn,
description: 'Visa Lambda Function ARN',
exportName: `VisaLambdaArn-${deploymentId}`,
});
}
}