Building an Amazon Bedrock AgentCore Agent Grounded in MongoDB Atlas Data
Introduction
This article is part of a series I have been writing about Amazon Bedrock AgentCore. Across the earlier articles, each component of the platform was introduced on its own and attached to the same running agent, from the two ways to deploy an agent, through the tools, memory, identity, and observability that surround it:
- Getting Started with Amazon Bedrock AgentCore Harness and Runtime
- Getting Started with Amazon Bedrock AgentCore Code Interpreter
- Getting Started with Amazon Bedrock AgentCore Gateway
- Getting Started with Observability for Amazon Bedrock AgentCore
- Getting Started with Amazon Bedrock AgentCore Web Search Tool
- Getting Started with Amazon Bedrock AgentCore Browser Tool
- Getting Started with Amazon Bedrock AgentCore Memory
- Getting Started with Amazon Bedrock AgentCore Knowledge Bases
- Getting Started with Amazon Bedrock AgentCore Identity
- Bringing It All Together: An Integrated Agent on Amazon Bedrock AgentCore
As a side note, between that series and this article I also took a detour to look at a new service from the other side of the aisle: MongoDB Atlas Agent Engine, which MongoDB launched into public preview in September 2026 as a managed runtime, memory, and governance layer for agents. In other words, a platform that promises to do much of what AgentCore does, but inside the MongoDB universe. That article explored it on its own terms, as a hello world on a brand new platform.
This article is the deliberate counterpoint to that detour. Instead of choosing one platform’s full stack over the other, it mixes the two worlds: keep Amazon Bedrock AgentCore as the agent platform, and use MongoDB Atlas purely as the governed operational data source behind it. This is the common real world situation. Many teams already run their operational data in Atlas and are not going to re-platform it to adopt an agent, and plenty of them are already committed to AWS for everything else. The interesting question is not which platform wins, it is how to connect a Bedrock AgentCore agent to data that already lives in Atlas, cleanly and with the access boundaries a security reviewer would accept.
A support agent that can only recite what a foundation model memorized during training is not very useful once a user asks about their own data. A streaming service’s subscriber asking “what did I think of that movie last year” or a bank customer asking for their balance both need the same thing: an agent that calls out to a real, governed data source instead of guessing.
Amazon Bedrock AgentCore gives you the pieces to build that agent: a managed runtime, a gateway that turns existing backends into tools the agent can call over MCP, and the identity plumbing to keep one caller from reaching another caller’s data. MongoDB Atlas is a natural fit as the data layer behind it, many teams already run their operational data there, and Atlas supports authenticating database users directly with AWS IAM, so a Lambda function can reach MongoDB with the same temporary credentials it already uses for everything else on AWS.
This article builds a small, concrete example: a member experience agent for a fictional streaming service. A logged in subscriber can ask general questions about the movie catalog, and can ask about their own reviews, grounded in real documents stored in Atlas. The same subscriber cannot retrieve another subscriber’s reviews, because the agent never accepts an identity parameter from the model, it only ever uses the identity that authentication already established.
What You Will Build
A chat agent hosted on AgentCore Runtime, backed by a model on Amazon Bedrock, that reaches two curated tools through AgentCore Gateway: one that searches a public movie catalog, and one that returns the signed in subscriber’s own reviews. Both tools are AWS Lambda functions that read from a MongoDB Atlas cluster using AWS IAM database authentication. Amazon Cognito handles subscriber login.
What You Will Learn
- How to prepare an Atlas cluster and inspect an existing dataset’s real schema before writing a single query against it
- How to authenticate a Lambda function to Atlas with AWS IAM credentials instead of a password
- How to expose Lambda tools to an agent through AgentCore Gateway, and host the agent as your own container on AgentCore Runtime
- How to make the caller’s identity, read from the validated token in the agent and not a model generated parameter, decide what data a tool returns
- How to validate and then observe the one behavior that matters most in a multi tenant agent: that one caller cannot read another caller’s data
Solution Architecture
Why Curated Tools Instead of a Generic Database Tool
MongoDB publishes an MCP Server that can expose generic find, aggregate, and schema inspection operations directly to an agent. That pattern is powerful for an internal, analyst facing agent, but it hands a model the ability to construct its own queries against a real database, which is exactly what a security reviewer will challenge for anything customer facing. This article takes the narrower path instead: a small number of Lambda functions, each with one fixed query shape, each one independently reviewable. The agent never sees a database driver, it only sees two named tools.
Why Identity Has To Come From the Session
The subscriber’s email is established once, at login, by Amazon Cognito. The Runtime validates that token at the edge and forwards it to the agent, which reads the email from it and injects it into the tool call. The identity scoped tool exposes no email parameter to the model, and the agent sets it from the session, so the model never chooses whose reviews come back and a subscriber reads their own and nobody else’s. This is the one design decision that makes the “can one user see another user’s data” question answerable with an architecture diagram instead of a promise about prompt wording.
Prerequisites
- An AWS account with access to Amazon Bedrock, AWS Lambda, Amazon Cognito, and Amazon Bedrock AgentCore in your chosen Region.
- A MongoDB Atlas account in an organization where you can create a project.
- Python 3.11 or later, and the AWS CLI configured with a profile that can create the resources above.
- Familiarity with pymongo, the official MongoDB driver for Python.
Step 1: Create the AWS IAM Role for the Lambda Functions
MongoDB Atlas database users of type AWS IAM authenticate with an AWS identity’s own credentials instead of a MongoDB specific password. A role is the better identity for this than a user: its credentials are temporary, instead of staying valid until someone remembers to rotate or delete them. This role has exactly one purpose, it is the Lambda functions’ execution role in Step 3, nothing else uses it.
1.1 Create the Role With a Trust Policy Scoped to Lambda
1.1.1. In the AWS Console, open IAM, then Roles, then click Create role.
1.1.2. Choose Custom trust policy, and replace the default statement with one that trusts only AWS Lambda, restricted to the two specific functions created in Step 3:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": { "Service": "lambda.amazonaws.com" },
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": { "aws:SourceAccount": "<your-account-id>" },
"ArnLike": {
"aws:SourceArn": [
"arn:aws:lambda:<your-region>:<your-account-id>:function:agentcore-demo-search-movies",
"arn:aws:lambda:<your-region>:<your-account-id>:function:agentcore-demo-list-my-reviews"
]
}
}
}
]
}aws:SourceArn matched against two exact function ARNs already implies the account on its own, aws:SourceAccount adds nothing strictly new here. AWS's own confused deputy guidance, and its own example trust policies for services such as AWS CloudFormation and AWS Transfer Family, still combine both conditions, evaluated independently, as the documented pattern, so this follows that convention rather than trimming it for being technically redundant. Fill in the function ARNs once you know the Region from Step 2.1.5, they are deterministic from account, Region, and the function names chosen in Step 3.
A Lambda function in a different AWS account could never use this role as its execution role in the first place, regardless of this condition: attaching a role to a function requires iam:PassRole permission on that exact role ARN, which only identities in your own account have by default. The condition is still worth having as defense in depth inside your own account, so that no other Lambda function you create later, even one of your own, can quietly reuse this same role just because it also lives in your account.
1.1.3. On the Add permissions screen that follows, select the Use existing policy tab, search for and check AWSLambdaBasicExecutionRole, then click Next. This has nothing to do with Atlas: AWS IAM authentication to Atlas only proves identity through AWS Security Token Service and needs no AWS permission at all. This one managed policy is the baseline every Lambda function needs regardless of what it does, permission to write its own logs to CloudWatch, required because this role is about to become the execution role for the two Lambda functions created in Step 3.
1.1.4. In Role name, enter atlas-demo-role. In Role description, enter Execution role for the Lambda tools that authenticate to MongoDB Atlas via AWS IAM, trusted only by the two Lambda functions in this build, then finish creation.
1.1.5. Copy the role’s ARN from the Summary section at the top of the role’s page, for example arn:aws:iam::<your-account-id>:role/atlas-demo-role. It is needed in Step 2.3 to create the matching Atlas database user.
The permission that actually matters, read only access to one sample database, is granted on the Atlas side, not in AWS; see Step 2.3.
Step 2: Prepare the Atlas Cluster and Inspect the Real Schema
2.1 Create the Project and a Free Tier Cluster
2.1.1. Sign in to MongoDB Atlas and create a dedicated project for this build, separate from any other work.
2.1.2. Under Database, click Clusters, then Create to open the cluster deployment page.
2.1.3. Select the Free tier (the same tier the API and documentation still refer to as M0). There is no reason to pay for anything beyond this for a learning build: Free tier clusters run MongoDB 8.0, well above the 7.0 minimum for AWS IAM authentication.
2.1.4. In Name, enter a descriptive name, cluster names cannot be changed after deployment. This article uses agentcore-demo.
2.1.5. Under Provider, select AWS, and pick the Region closest to where you will run Bedrock and Lambda.
2.1.6. Under Quick setup, keep Automate security setup and Preload sample dataset checked, then click Create Deployment. Provisioning takes a minute or two.
A guided setup screen prompting you to create a database user and offering a ready made connection snippet to download sometimes appears right after the cluster goes active. It showed up this way in a freshly created test account while writing this article, and did not appear in another account that already had unrelated clusters running in the same organization. This was not tracked down to a documented cause, treat it as something that may or may not happen for you: use it if it appears, otherwise create the user manually in Step 2.3.
2.2 Look at the Data Before Writing Any Query
Quick Setup currently preloads a single sample database, sample_mflix, a movie catalog with user generated reviews. Older articles and some documentation still describe a broader library of sample databases including sample_analytics; that is not what the console loads today, so confirm what you actually have before writing a query against it.
2.2.1. The fastest way to look at real documents, no code required, is the Clusters page. Click Browse Collections on your cluster’s card.
2.2.2. This opens Data Explorer directly on a collection. The sidebar lists all six collections in sample_mflix: movies, embedded_movies, comments, users, theaters, and sessions.
Two of them matter for this build. movies is the public catalog: title, plot, genres, cast, year, imdb.rating, among other fields, no identity needed to read it. comments holds one subscriber review per document, with name, email, movie_id (a reference into movies), text, and date, the email field is what links a review back to a subscriber.
The other three are deliberately not used: sessions holds synthetic sample data whose user_id does not correspond to real users, it is not a usable identity mechanism, Amazon Cognito plays that role in this build. theaters is physical cinema location data, unrelated to this use case. embedded_movies duplicates movies with a precomputed plot embedding, useful for a future Atlas Vector Search upgrade, not needed for the first version.
2.3 Create the Atlas Database User and Open Network Access
2.3.1. Under Database Access, click Add New Database User and create the one the Lambda functions will use:
- Authentication Method: AWS IAM.
- AWS IAM Type: IAM Role.
- AWS Role ARN: the role’s ARN from Step 1.1.5, for example
arn:aws:iam::<your-account-id>:role/atlas-demo-role. - User Description (the field’s own placeholder reads “Add an optional description to your user”): enter
Read only identity for the Lambda tools that query sample_mflix, mapped to atlas-demo-role.
Under Database User Privileges, use Specific Privileges: click Add Specific Privilege, choose read, set the database to sample_mflix, and leave the collection blank. This is the only identity the Lambda functions use in Step 3.
2.3.2. Under Network Access, click Add IP Address. In the Access List Entry field, enter 0.0.0.0/0, and in Comment (optional, but worth leaving a trace of why such a wide entry exists), enter Allow all for demo only, see article for justification. Leave This entry is temporary off, and click Confirm.
This is a deliberate shortcut, not an oversight. The caller here is Amazon Bedrock AgentCore, running on AWS managed infrastructure with no fixed outbound IP of its own, so no single address can be allowlisted instead. The two Atlas features built for exactly this problem, VPC Peering and PrivateLink, both require a dedicated cluster tier, M10 or above, not the Free tier this build uses; peering connections specifically are not available on Free tier clusters at all. The risk is bounded here: the database identity is read only, scoped to one public sample dataset with no real customer data, on a cluster that gets deleted at the end. Do not carry this choice into production. There, move to a dedicated cluster with PrivateLink or VPC Peering, or at minimum put the Lambda functions in a VPC with a NAT Gateway and a single Elastic IP, so one specific address can be allowlisted instead of the whole internet.
Step 3: Build the Deployment Zips and Create the Lambda Functions
The role created in Step 1 already has the trust policy and the CloudWatch Logs permission it needs, and the Atlas database user from Step 2.3.1 already maps to it. This step builds the two deployment zips locally, then creates the two functions that role was scoped for and wires each one to Atlas, there is no second role and no second Atlas database user to create.
3.1 Clone the Repository, Read the Code, and Build the Deployment Zips
3.1.1. Clone the companion repository and move into it:
git clone https://github.com/biagolini/PythonAwsBedrockAgentCoreMongoDBAtlas.git
cd PythonAwsBedrockAgentCoreMongoDBAtlasRather than reproduce every line here, this section describes the pieces of the repository that Step 3 depends on; read the code directly in the repository, each file is short and commented. The two tools relevant to this step live in their own folders, with no code shared between them:
search_movies/holds the public catalog tool: alambda_function.pythat queriessample_mflix.moviesand returns a handful of fields, plus its ownrequirements.txt(justpymongo[aws]) and a shortREADME.md.list_my_reviews/holds the identity scoped tool: alambda_function.pythat returns the signed in subscriber's own reviews fromsample_mflix.comments, joined with the movie title, plus the samerequirements.txtandREADME.md.build.shat the repository root builds one deployment zip per tool, andREADME.mddocuments the layout and the deploy steps.
A few design choices in those lambda_function.py files are worth calling out before you run them, because they are the difference between a demo that fails cleanly and one that hangs or leaks:
- Both keep the MongoDB client outside the handler, the documented pattern for Lambda in the absence of a MongoDB equivalent of RDS Proxy, with a short
serverSelectionTimeoutMSso a real connectivity problem fails in a few seconds instead of pymongo's 30 second default. - Both catch
PyMongoError, log the real diagnosis to CloudWatch, and return a short, generic message to the calling agent instead, which has no access to this article and may relay that text to the end user verbatim. list_my_reviewsreceives the caller's email as a tool argument that the agent container injects from the validated session (Step 6), never a value the model chose, which is the single decision that makes one subscriber unable to request another subscriber's reviews. It also masks the email in its own log lines (the full address never appears in the function's CloudWatch logs); note that the OpenTelemetry tool-call span records the argument in full, which Step 9.3 addresses.
The exact shape of the event AgentCore Gateway sends to a Lambda target depends on how you configure the target, confirm it against the current AgentCore Gateway documentation before finalizing this.
3.1.2. Run the build script:
./build.shIt installs pymongo[aws], pinned to 4.18.2, the version this article was tested against, targeting Lambda's arm64 architecture and Python 3.14, regardless of your own machine's architecture, and writes two self contained zips: dist/search_movies.zip and dist/list_my_reviews.zip.
If you change either lambda_function.py, rerun this script and re-upload the affected zip via Step 3.2.3 (or the matching action inside 3.2.6 for the other function), no other step in this article needs to change.
3.2 Create Each Function
3.2.1. In the Lambda console, under Event Functions, click Create Event Function.
Keep Author from scratch selected, and fill in:
- Function name:
agentcore-demo-search-movies. Theagentcore-demo-prefix only makes the function easy to spot if your account already has other, unrelated Lambda functions in it, it has no technical meaning. - Runtime: Python 3.14.
3.2.2. Expand Additional settings, and under General, turn on ARM64 architecture, and turn on Custom execution role, click Edit, and select the existing atlas-demo-role from Step 1 instead of letting AWS create a new one. Click Create Event Function.
3.2.3. Once the function exists, replace its placeholder code: on the Code tab, click Update, then Update from a .zip file, and choose dist/search_movies.zip, the one you just built in 3.1.2. The console suggests uploading through Amazon S3 for files over 10 MB, this one is about 18 MB and still uploads directly without issue.
3.2.4. Under Configuration > General configuration, click Edit and set Timeout to 15 sec. The default, 3 seconds, is shorter than the 5 second serverSelectionTimeoutMS set in the client code (3.1.1), leaving no room for the rest of the handler to run. 15 seconds is not just that 5 seconds plus margin: a real failure against this cluster was measured at close to 8.5 seconds end to end, pymongo's own selection loop finishes an in-flight scan round before it checks the deadline, so the actual wait runs a few seconds past the configured value.
3.2.5. Under Configuration > Environment variables, add the connection string, with no password:
MONGODB_URI="mongodb+srv://<your-cluster-host>/?authSource=%24external&authMechanism=MONGODB-AWS"3.2.6. Repeat 3.2.1 through 3.2.5 for agentcore-demo-list-my-reviews, same role, same connection string. This one is a separate Lambda with its own code, so it gets its own zip, dist/list_my_reviews.zip, not the one from 3.2.3.
A Lambda function automatically receives its execution role’s temporary credentials through its environment, pymongo resolves them through the same boto3 credential chain the AWS CLI uses, there is nothing else to configure for AWS IAM authentication.
Network access needs no further change, the 0.0.0.0/0 entry from Step 2.3.2 already covers both functions' connections too.
AWS IAM database authentication worked on the Free tier in this build, confirmed end to end by the test in Step 3.3. The Free Cluster Limits page lists AWS IAM among the supported authentication methods for Free clusters. If a particular account ever rejects it, fall back to a password stored in AWS Secrets Manager for this build, and keep IAM authentication as the production target.
3.3 Test Each Function Against Atlas
Before wiring anything to Cognito, Gateway, or Runtime, confirm each function can actually reach Atlas on its own.
3.3.1. On agentcore-demo-search-movies, open the Test tab, create a new test event named search-comedies, and replace its JSON with the flat argument map the Gateway sends (the handler also accepts an input wrapper, but flat is the real shape):
{
"genre": "Comedy",
"limit": 2
}Click Test. A successful run returns a handful of real documents from sample_mflix.movies in the response, not an error, confirming the function reaches Atlas and the AWS IAM authentication from Step 2.3.1 works end to end.
3.3.2. On agentcore-demo-list-my-reviews, open its Test tab, create a test event named list-my-reviews-test, using a real email address from the comments collection, found the same way as in Browse Collections back in Step 2.2, jordan_medina@fakegmail.com in this build:
{
"email": "jordan_medina@fakegmail.com"
}Click Test. A successful run returns that subscriber’s reviews, joined with the movie title, confirming both the connection and the aggregation pipeline work before anything upstream exists yet.
If either test fails, the Test tab only shows a short, generic error on purpose, see 3.1.1. Open the function’s CloudWatch Logs for the actual diagnosis, and check Network Access is 0.0.0.0/0 (Step 2.3.2) and that the function's MONGODB_URI and role match what Steps 1 and 2.3.1 created.
Step 4: Create the Cognito User Pool for Subscriber Login
4.1 Create the User Pool
The chat client authenticates subscribers directly against the Cognito API, so the App Client must be created without a client secret. The Single-page application (SPA) type does exactly that.
4.1.1. Navigate to Amazon Cognito > User pools and click Create user pool.
4.1.2. On the Set up resources for your application page, fill it in as follows:
Define your application:
- Application type: select Single-page application (SPA), which creates the App Client without a client secret.
- Name your application: enter
agentcore-identity-demo.
Configure options:
- Options for sign-in identifiers: check Username only (leave Email and Phone number unchecked). Subscribers sign in by username; the
emailattribute is what links each one to their reviews in thecommentscollection. - Self-registration: uncheck Enable self-registration, you will create the test users manually.
- Required attributes for sign-up: select
email,name, andphone_numberfrom the dropdown.
Add a return URL: leave the Return URL field empty, this build authenticates through the Cognito API directly rather than the hosted UI.
4.1.3. Click Create user directory.
4.1.4. A page opens showing code examples for React, Angular, and JavaScript. Scroll to the bottom and click Go to overview. After creation, note your User Pool ID at the top of the pool details page, in the format us-east-1_XXXXXXXXX.
4.2 Configure the App Client
The Create user directory step already created an App Client without a client secret. Enable the USER_PASSWORD_AUTH flow so the chat client can sign subscribers in programmatically.
4.2.1. Expand the navigation panel, then go to App integration > App clients and click the app client named agentcore-identity-demo.
4.2.2. In the Authentication flows section, click Edit.
4.2.3. Enable ALLOW_USER_PASSWORD_AUTH. Keep the flows already enabled by default (ALLOW_USER_AUTH, ALLOW_USER_SRP_AUTH, ALLOW_REFRESH_TOKEN_AUTH), then click Save changes.
4.2.4. Copy the Client ID shown at the top of the app client page, and confirm that Client secret is empty. You will need this Client ID in Step 7 when wiring the Runtime inbound JWT authorizer, and in the chat client later.
4.3 Create the Test Subscribers
Create the test subscribers with the AWS CLI. Each one signs in by username, but the email attribute must match a real email value already present in the comments collection, so the demo has reviews to retrieve. Browse Collections from Step 2.2 shows these values directly; this build uses jordan_medina@fakegmail.com, the same address tested in Step 3.3.2.
4.3.1. Set your User Pool ID once, then run the commands for each subscriber, substituting an email that exists in comments:
export POOL_ID=us-east-1_XXXXXXXXXaws cognito-idp admin-create-user \
--user-pool-id $POOL_ID \
--username jordan \
--user-attributes Name=name,Value="Jordan Medina" Name=email,Value="jordan_medina@fakegmail.com" Name=email_verified,Value=true Name=phone_number,Value="+12025550101" Name=phone_number_verified,Value=true \
--message-action SUPPRESSaws cognito-idp admin-set-user-password \
--user-pool-id $POOL_ID \
--username jordan \
--password "Subscriber1Pass!" \
--permanent
This build has no Cognito groups and no role based authorization. Cognito plays a single part here: it is the authentication layer, the gate that lets a subscriber reach the agent at all. Once a subscriber is authenticated, every one of them has the same access to the two tools; what differs between them is only the data each tool returns, because list_my_reviews is scoped to the email carried from that subscriber's own authenticated session. There is nothing group related or role related to configure.
4.4 Note Your Discovery URL
The Runtime inbound JWT authorizer in Step 7 needs the OIDC discovery URL to fetch Cognito’s public keys and validate tokens. Find it on the user pool’s Overview page, under the OpenID Connect configuration URL field.
The URL follows this pattern:
https://cognito-idp.<your-region>.amazonaws.com/<your-pool-id>/.well-known/openid-configurationPaste it in a browser to confirm it returns a JSON document with issuer, jwks_uri, and the other OIDC metadata. Save this URL and the Client ID: both are used in Step 7 by the Runtime inbound JWT authorizer, which validates the subscriber's token at the edge. The agent container then reads the email claim from that validated token and injects it into the tool call (Step 6).
Step 5: Expose the Tools with AgentCore Gateway
AgentCore Gateway turns the two Lambda functions into MCP tools the agent can discover and call, behind a single endpoint with inbound authentication. The agent never sees a Lambda ARN or a database driver, only two named tools.
5.1 Create the Gateway IAM Role
The Gateway needs a role it can assume to invoke the two Lambda functions on the agent’s behalf.
5.1.1. In the AWS Console, open IAM > Roles, click Create role, and select Custom trust policy. Paste a policy that lets the AgentCore service assume the role:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": { "Service": "bedrock-agentcore.amazonaws.com" },
"Action": "sts:AssumeRole"
}
]
}5.1.2. Skip the managed policies and click Next. Name the role agentcore-mongodb-gateway-role and create it.
5.1.3. Open the new role, click Add permissions > Create inline policy, select the JSON tab, and enter a policy that allows the Gateway to read its own configuration and invoke only the two functions from Step 3:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "GetGateway",
"Effect": "Allow",
"Action": ["bedrock-agentcore:GetGateway"],
"Resource": "arn:aws:bedrock-agentcore:<your-region>:<your-account-id>:gateway/*"
},
{
"Sid": "LambdaInvoke",
"Effect": "Allow",
"Action": "lambda:InvokeFunction",
"Resource": [
"arn:aws:lambda:<your-region>:<your-account-id>:function:agentcore-demo-search-movies",
"arn:aws:lambda:<your-region>:<your-account-id>:function:agentcore-demo-list-my-reviews"
]
}
]
}Name the policy GatewayLambdaAccess and create it.
5.2 Create the Gateway
The Gateway creation wizard has four steps: define the gateway, configure inbound authorization, add targets, and review.
5.2.1. Open the Amazon Bedrock AgentCore console, and under Build, click Gateways, then Create Gateway.
5.2.2. Step 1, define details. In Gateway name, enter MongoDBAtlasGateway. The protocol is MCP, the single protocol a Gateway speaks, so there is nothing to choose there. Under Permissions, select Use another role and choose agentcore-mongodb-gateway-role from Step 5.1. Leave the optional sections collapsed and click Next.
5.2.3. Step 2, inbound authorization. Think about who actually calls this Gateway. It is not the subscriber’s browser; it is the agent container, which signs its calls with the AWS credentials of its Runtime execution role (see agent.py in Step 6, which uses an IAM SigV4 MCP client). So the Gateway authenticates a service caller, not an end user. Select Authenticate only, which verifies the caller's identity with AWS Signature Version 4 and leaves authorization to the target and the gateway role. The end user's Cognito token is validated upstream, at the Runtime's inbound JWT authorizer in Step 7, not here. Click Next.
The other options are either stricter than this build needs or wrong for it. Use IAM permissions also uses SigV4 and would work too; it additionally enforces IAM authorization at the gateway, which is fine but redundant here since the Runtime already gates access and the gateway role already scopes the Lambda invocations. Use JSON Web Tokens (JWT) would be right only if an external client called the Gateway directly with an OAuth token, but then the SigV4 call from the agent would be rejected. No authorization leaves the Gateway publicly accessible, which this build avoids.
Get Carlos Biagolini’s stories in your inbox
Join Medium for free to get updates from this writer.
5.2.4. Step 3, add the two targets. A target is one backend exposed as MCP tools. For each one, click Add target and fill in:
- Target name: a short name using letters, numbers, and hyphens only (
search-moviesandlist-my-reviews). The target name cannot contain underscores, so it differs from the tool name inside the schema below, which stayssearch_moviesandlist_my_reviews. The Gateway prepends the target name to the tool name the agent sees. - Target type: Lambda. The other target types (OpenAPI, Smithy, MCP server, connector) are for different backends; a Lambda function is a Lambda target.
- Lambda ARN: the ARN of the matching function from Step 3.
- Outbound authorization / credential provider: the Gateway invokes the Lambda using its own IAM role, so this is the Gateway IAM role (
GATEWAY_IAM_ROLE), theagentcore-mongodb-gateway-rolefrom Step 5.1. Lambda targets only support IAM role authentication. - Tool schema: select Inline and paste the JSON below. This is the contract the model sees, so what you leave out of it matters as much as what you put in.
For the search-movies target, use the ARN of agentcore-demo-search-movies and this schema:
[
{
"name": "search_movies",
"description": "Search the public movie catalog by optional title fragment and optional genre. Use this for any general question about movies: plots, genres, cast, release year, rating.",
"inputSchema": {
"type": "object",
"properties": {
"title": { "type": "string", "description": "Case-insensitive title fragment to match" },
"genre": { "type": "string", "description": "Exact genre, for example Comedy or Drama" },
"limit": { "type": "integer", "description": "Maximum number of movies to return" }
},
"required": []
}
}
]For the list-my-reviews target, use the ARN of agentcore-demo-list-my-reviews and the schema below. Notice there is no email property: the model never sees a field for whose reviews to return. The agent container injects the subscriber's email into the call from the validated session (Step 6, bind_identity), and the Gateway forwards that argument to the Lambda even though it is not in the schema (confirmed by observation in this build: a call carrying email reaches the Lambda with the key present). Keeping email out of the schema is the stronger design, because the model cannot name a parameter it cannot see, so it cannot even attempt to ask for someone else's reviews:
[
{
"name": "list_my_reviews",
"description": "Return the signed in subscriber's own movie reviews, most recent first. Use this when the subscriber asks about their own reviews or what they thought of a movie. It only ever returns the current subscriber's reviews; it cannot look up anyone else.",
"inputSchema": {
"type": "object",
"properties": {
"limit": { "type": "integer", "description": "Maximum number of reviews to return" }
},
"required": []
}
}
]The description field is what the model reads to decide when to call each tool, so keep both clear and distinct. Click Next.
5.2.5. Step 4, review and create. Confirm the gateway name, the Authenticate only inbound authorization, and both targets, then click Create. Wait for the Gateway and both targets to reach Ready.
5.3 Note the Gateway URL
5.3.1. On the Gateway details page, copy the Gateway URL (the MCP endpoint). The agent container reads it as the GATEWAY_URL environment variable in Step 7.
Step 6: Build and Push the Agent Container to Amazon ECR
The agent runs as your own container on AgentCore Runtime. This is the deliberate choice over the simpler managed Harness: only with your own code in the loop can the subscriber’s identity be read from the validated token and injected into the tool call, instead of being a parameter the model fills in. That single property is what makes “one subscriber cannot read another’s reviews” a fact of the architecture rather than a hope about prompt wording.
6.1 Read the Agent Code
The companion repository’s agent/ folder holds the container. As with the Lambda tools, the code is short and commented; this section describes the pieces that matter for the build rather than reproducing them.
agent.pyis a LangGraphcreate_react_agent. LangGraph here is deliberate: the rest of the AgentCore series used the Strands Agents SDK, and the companion MongoDB Atlas Agent Engine walkthrough used LangGraph, so it is the common ground between the two worlds this article mixes. AgentCore Runtime is framework agnostic, so the choice is free.requirements.txtpins the dependencies, includingaws-opentelemetry-distrofor the observability in Step 9.Dockerfilebuilds an ARM64 image that serves on port 8080 and starts underopentelemetry-instrument, so traces flow to CloudWatch with no extra code.
Three things in agent.py carry the design:
extract_claims(context)reads theAuthorizationheader, strips theBearerprefix, and decodes the JWT without re-verifying the signature. It does not need to re-verify: the Runtime already validated the token at the edge before the request reached the container. The header only arrives at all because the Runtime declares it in the request header allowlist, configured in Step 7.3.bind_identity(tools, email)is the heart of the governance story. It wraps thelist_my_reviewstool so every call carries theemailread from the session. The Gateway tool schema from Step 5.2.4 exposes noemailfield to the model, so the model cannot name it, and the wrapper is the only thing that sets it.search_moviesis left untouched, since the public catalog needs no identity. The result: no path for the model to choose whose reviews come back.- The model is read from the
BEDROCK_MODEL_IDenvironment variable. This build uses an Amazon Nova model, billed as native AWS usage. To align the AWS side with the Claude model used on the MongoDB Atlas Agent Engine side, set this variable to a Claude inference profile instead and widen the Bedrock permission in Step 7.1; note that third party models are billed through AWS Marketplace, which many promotional credits do not cover.
6.2 Create the ECR Repository
6.2.1. Navigate to Amazon ECR > Repositories and click Create repository.
6.2.2. Keep Private visibility, and in Repository name enter agentcore/mongodb-atlas-agent. The agentcore/ namespace groups all agent images together. Leave the other settings at their defaults and click Create repository.
6.2.3. Open the repository and copy its URI, in the form <your-account-id>.dkr.ecr.<your-region>.amazonaws.com/agentcore/mongodb-atlas-agent.
6.3 Build and Push the Image
Building for AgentCore Runtime’s required linux/arm64 platform, the image is created and pushed with Docker buildx. Run this from the repository root, after setting your account and region:
export AWS_REGION=us-east-1
export AWS_ACCOUNT_ID=<your-account-id>
export REPO_URI="${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/agentcore/mongodb-atlas-agent"aws ecr get-login-password --region $AWS_REGION \
| docker login --username AWS --password-stdin "${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com"docker buildx build --platform linux/arm64 --tag "${REPO_URI}:latest" --push code/agent
Copy the full image URI (${REPO_URI}:latest); you need it when creating the Runtime in Step 7.2.
Step 7: Host the Agent on AgentCore Runtime
With the image in ECR, create the Runtime that hosts it, validates the inbound Cognito token, and forwards the Authorization header to the container.
7.1 Create the Runtime Execution Role
7.1.1. In IAM > Roles, click Create role, select Custom trust policy, and paste a policy that trusts the AgentCore service in your account:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "sts:AssumeRole",
"Principal": { "Service": "bedrock-agentcore.amazonaws.com" },
"Condition": { "StringEquals": { "aws:SourceAccount": "<your-account-id>" } }
}
]
}7.1.2. Name the role agentcore-mongodb-runtime-role and create it. Then add an inline policy with the permissions the Runtime needs to pull the image, write logs and traces, publish metrics, invoke the model, and obtain its workload identity (replace account and region):
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ECRAuth",
"Effect": "Allow",
"Action": ["ecr:GetAuthorizationToken"],
"Resource": "*"
},
{
"Sid": "ECRImage",
"Effect": "Allow",
"Action": ["ecr:BatchGetImage", "ecr:GetDownloadUrlForLayer"],
"Resource": "arn:aws:ecr:<your-region>:<your-account-id>:repository/agentcore/mongodb-atlas-agent"
},
{
"Sid": "Logs",
"Effect": "Allow",
"Action": ["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents", "logs:DescribeLogStreams", "logs:DescribeLogGroups"],
"Resource": "arn:aws:logs:<your-region>:<your-account-id>:log-group:/aws/bedrock-agentcore/runtimes/*"
},
{
"Sid": "XRay",
"Effect": "Allow",
"Action": ["xray:PutTraceSegments", "xray:PutTelemetryRecords", "xray:GetSamplingRules", "xray:GetSamplingTargets"],
"Resource": "*"
},
{
"Sid": "Metrics",
"Effect": "Allow",
"Action": ["cloudwatch:PutMetricData"],
"Resource": "*",
"Condition": { "StringEquals": { "cloudwatch:namespace": "bedrock-agentcore" } }
},
{
"Sid": "BedrockNova",
"Effect": "Allow",
"Action": ["bedrock:InvokeModel", "bedrock:InvokeModelWithResponseStream"],
"Resource": [
"arn:aws:bedrock:*::foundation-model/amazon.nova-*",
"arn:aws:bedrock:<your-region>:<your-account-id>:inference-profile/*amazon.nova-*"
]
},
{
"Sid": "WorkloadIdentity",
"Effect": "Allow",
"Action": ["bedrock-agentcore:GetWorkloadAccessToken", "bedrock-agentcore:GetWorkloadAccessTokenForJWT"],
"Resource": [
"arn:aws:bedrock-agentcore:<your-region>:<your-account-id>:workload-identity-directory/default",
"arn:aws:bedrock-agentcore:<your-region>:<your-account-id>:workload-identity-directory/default/workload-identity/*"
]
},
{
"Sid": "InvokeGateway",
"Effect": "Allow",
"Action": ["bedrock-agentcore:InvokeGateway"],
"Resource": "arn:aws:bedrock-agentcore:<your-region>:<your-account-id>:gateway/*"
}
]
}The InvokeGateway statement is what lets the agent reach the Gateway. The Gateway uses IAM (SigV4) inbound authorization from Step 5.2.3, so the agent's SigV4 call is authorized by this permission on its execution role. Scope the Resource to your specific gateway ARN once you have it, as a best practice.
7.2 Create the Runtime
7.2.1. Navigate to Amazon Bedrock AgentCore > Runtime and click Create runtime.
7.2.2. In Name, enter agentcore_mongodb_demo (letters, numbers, and underscores only, no hyphens).
7.2.3. For Runtime platform version, select V2. It starts each session from a prepared snapshot, so cold starts are fast and consistent, and it bills the same way as V1 (see Platform versions). Creating a V2 runtime takes a few minutes rather than seconds, while the snapshot is prepared.
7.2.4. Choose the Compute type. Select microVMs, the serverless, pay per use model where each session runs in an isolated microVM and the agent starts on demand, which is the right fit for a real time chat agent. The alternative, Instances, provisions AWS managed EC2 for persistent, multi-day, or GPU workloads, which this build does not need. You cannot change the compute type after the runtime is created.
7.2.5. Under Agent source, select ECR Container and paste the image URI from Step 6.3.
7.2.6. Under Permissions, select Use another role and choose agentcore-mongodb-runtime-role from Step 7.1.
7.2.7. Under Inbound Auth, configure the JWT authorizer that validates the subscriber’s Cognito token:
- Protocol: select HTTP.
- Inbound Auth type: select Use JSON Web Tokens (JWT).
- JWT schema configuration: select Use existing Identity provider configurations.
- Discovery URL: paste the Cognito OIDC URL from Step 4.4 (
https://cognito-idp.<your-region>.amazonaws.com/<your-pool-id>/.well-known/openid-configuration). - Allowed audiences: check this box and enter the App Client ID from Step 4.2.4 (
<your-app-client-id>, a value like5r4a1l3135ilp1riba0bu3inar). The authorizer requires at least one of allowed audiences, allowed clients, allowed scopes, or custom claims; this one satisfies that, because a Cognito ID token carries the App Client ID in itsaudclaim. - Allowed clients: leave unchecked. Cognito ID tokens have no
client_idclaim (that is on access tokens), so enabling this would reject every request. - Allowed scopes: leave unchecked. The ID token this build uses does not carry the scopes this field expects.
7.2.8. Leave Filesystem configuration empty. It is optional, and it only matters for agents that write files they need to keep across session stop and resume cycles. This agent is stateless; the durable data lives in MongoDB Atlas, not in the container filesystem, so no mount is needed.
7.2.9. Under Advanced configurations, keep the endpoint Public and add two environment variables: BEDROCK_MODEL_ID set to a Nova model available in your Region (for example us.amazon.nova-2-lite-v1:0, confirm it against the Bedrock model support by Region), and GATEWAY_URL set to the Gateway URL from Step 5.3. These two variables are mandatory: the agent reads them at startup, so a missing one makes the container exit immediately and the runtime creation fail.
7.2.10. Click Create runtime and wait for the status to reach Ready. With V2 this takes a few minutes while the snapshot is prepared.
If the runtime ends in CREATE_FAILED with “The runtime process exited unexpectedly”, the most common cause with V2 is a missing environment variable from Step 7.2.9. The agent reads BEDROCK_MODEL_ID and GATEWAY_URL at import time, so if either is absent the process raises KeyError and exits before it can report healthy, and the snapshot never completes. Check the runtime's CloudWatch logs for the traceback, confirm both variables, and recreate. You can also add or fix them later with the update-agent-runtime command shown in Step 7.3.
7.3 Forward the Authorization Header to the Container
The Runtime strips the Authorization header after it validates the token, so by default the container never sees it and extract_claims would find nothing. You forward it by adding it to the request header allowlist. At the time of writing this option was not in the console, so it is configured with the AWS CLI. The update is a full replacement, so every field is included even when unchanged, otherwise the omitted ones reset to empty:
aws bedrock-agentcore-control update-agent-runtime \
--agent-runtime-id <your-runtime-id> \
--role-arn arn:aws:iam::<your-account-id>:role/agentcore-mongodb-runtime-role \
--agent-runtime-artifact '{"containerConfiguration":{"containerUri":"<your-image-uri>"}}' \
--network-configuration '{"networkMode":"PUBLIC"}' \
--authorizer-configuration '{"customJWTAuthorizer":{"discoveryUrl":"<your-discovery-url>","allowedAudience":["<your-client-id>"]}}' \
--request-header-configuration '{"requestHeaderAllowlist":["Authorization"]}' \
--environment-variables '{"BEDROCK_MODEL_ID":"us.amazon.nova-2-lite-v1:0","GATEWAY_URL":"<your-gateway-url>"}' \
--platform-version V2 \
--region us-east-1Include --platform-version V2 so this full replacement keeps the runtime on V2; omitting it would behave as if the field were cleared. The update prepares a fresh V2 snapshot, so expect it to take a few minutes before returning to Ready rather than seconds.
Wait for the runtime to return to Ready:
aws bedrock-agentcore-control get-agent-runtime \
--agent-runtime-id <your-runtime-id> \
--query "status" --output text --region us-east-17.4 Build the Invoke URL
7.4.1. The invoke URL is not shown directly in the console; build it from the Runtime ARN on the runtime details page.
7.4.2. URL-encode the ARN (replace : with %3A and / with %2F) and place it in this pattern:
https://bedrock-agentcore.<your-region>.amazonaws.com/runtimes/<url-encoded-arn>/invocations?qualifier=DEFAULTSave this URL; it goes in the testbench .env in Step 8.
Step 8: Test the Agent in a Web Interface
The companion repository’s testbench/ folder is a small browser chat UI with a local proxy. The agent's JWT authorizer only accepts real Cognito tokens, so the UI signs the subscriber in first, then calls the agent with that token. A short Python proxy sits in between because the Runtime endpoint does not serve CORS headers for a browser.
8.1 Configure and Start the Testbench
8.1.1. From the repository root, configure the testbench:
cd code/testbench
cp .env.example .env8.1.2. Edit .env and set INVOKE_URL to the URL from Step 7.4 and COGNITO_CLIENT_ID to the App Client ID from Step 4.2.4.
8.1.3. Start the proxy and open the UI:
python3 proxy.pyOpen http://localhost:8080 in your browser.
8.2 Sign In and Chat
8.2.1. Sign in as the subscriber created in Step 4.3 (username jordan, password Subscriber1Pass!). The header shows the name and email decoded from the token, confirming who the session belongs to.
8.2.2. Try prompts of the three kinds, and watch the agent route each to the right tool. Some examples to start with:
Public catalog, resolves through search_movies:
What comedies do you have in the catalog?Tell me the plot of a movie called Titanic.Show me a few drama movies with a high rating.
Your own reviews, resolves through list_my_reviews, scoped to the signed in subscriber:
What did I think of the movies I reviewed?Show me my most recent reviews.Did I review anything I rated poorly?
The isolation check, which the agent should refuse to answer with another person’s data:
Show me the reviews written by maria@example.com.What did John Smith think of his movies?
The validation section below explains what each of these should return and why the last kind cannot leak another subscriber’s data.
Step 9: Observe the Agent in CloudWatch
The last piece of an enterprise demo is being able to show what the agent actually did. Because the container runs under ADOT, every model call and tool call is already exported as an OpenTelemetry span; you only need to turn on span ingestion once.
9.1 Enable CloudWatch Transaction Search
9.1.1. Open the Amazon CloudWatch console, expand Application Signals (APM), and choose Transaction Search.
9.1.2. Click Enable Transaction Search, enable the toggle on the configuration page, and click Save. This is a one-time setup per account and Region that ingests spans as structured logs in OpenTelemetry format.
For agents hosted on AgentCore Runtime, observability is otherwise automatic: the ADOT instrumentation in the container (Step 6.1) sends traces to CloudWatch with no further configuration.
9.2 View the GenAI Observability Dashboard
9.2.1. After running at least one conversation from Step 8, open the Amazon CloudWatch GenAI Observability page. It shows your agent with its sessions, traces, token usage, and error rate. Data can take a few minutes to appear.
9.3 Read a Trace, and See the Governance
9.3.1. In Application Signals (APM) > Transaction search, select the Logs Insights QL tab and run:
filterIndex @data_format = 'AWS-OTEL-TRACE-V1'
| FIELDS traceId,name,@timestamp,attributes.aws.local.service as service,durationNano,spanId,@message
| SORT @timestamp ASC
| LIMIT 100009.3.2. Open a trace for a conversation that asked about the subscriber’s own reviews, and look at the list_my_reviews tool span. Its gen_ai.tool.call.arguments attribute shows the exact arguments the tool ran with, including the email the container injected from the session (and the limit). This is the point of the whole build for an enterprise reviewer: the identity the tool acted on is visible in the trace, it came from the authenticated session, and it was never something the conversation could set. "Trust me, it is governed" becomes a span you can point at.
One honest caveat about that same span: it stores the email in full, in gen_ai.tool.call.arguments, and the tool result text alongside it. The Lambda deliberately masks the email in its own log lines, but the span does not, and with CloudWatch Transaction Search enabled the span lives in CloudWatch Logs (aws/spans). That is a deliberate audit-versus-PII trade off: the full identity in the trace is what makes the run auditable, but in production you would mask or drop sensitive span attributes, or restrict access to the span log group, so personal data is not retained in the clear.
Validation
Run three conversations against the deployed agent, signed in through the testbench from Step 8.
- A general catalog question, such as asking about a movie’s plot or cast. This should resolve through
search_movies, which needs no identity: every subscriber sees the same public catalog. - A personal question, such as asking for your own recent reviews. This should resolve through
list_my_reviews, scoped to the signed in subscriber's email. - The one that matters most: ask the agent for another subscriber’s reviews, by name or email. It cannot produce them.
list_my_reviewsruns only with the email the container injected from the authenticated session, and the model-facing tool exposes no email field, so there is nothing a prompt could set to reach someone else's reviews. The model declines, as validated across repeated runs. Step 9.3 shows this same guarantee from the other side, in the trace.
Cleanup
- Delete the AgentCore Runtime (Amazon Bedrock AgentCore > Runtime).
- Delete the AgentCore Gateway and its two targets.
- Delete the Amazon ECR repository
agentcore/mongodb-atlas-agent, including the image. - Delete both Lambda functions.
- Delete the Cognito user pool.
- In Atlas, remove the AWS IAM database user mapped to the role from Step 1, then terminate the cluster from the Clusters page.
- In IAM, delete the three roles created for this build:
atlas-demo-role(Step 1),agentcore-mongodb-gateway-role(Step 5.1), andagentcore-mongodb-runtime-role(Step 7.1). Your own AWS identity is yours to keep, nothing about it was created for this build. - Delete the local
code/testbench/.envfile, it still holds the invoke URL and the client ID.
Next Steps
- Move the demo cluster to a dedicated tier with PrivateLink or VPC Peering, removing the
0.0.0.0/0entry from Step 2.3.2 entirely. - Add Atlas Vector Search over
embedded_moviesso the agent can match plots semantically and make recommendations, instead of exact field matches only. - Replace the single Lambda per tool pattern with a shared data access layer (for example, a small service on Amazon ECS with its own connection pool) once the number of tools and the required throughput both grow, exposed to the Gateway as an OpenAPI target instead of individual functions.
- Add AgentCore Memory so the agent recalls a subscriber’s taste across sessions, scoped by the same session identity used here.
Conclusion
The interesting part of this build was never the model. It was keeping the subscriber’s identity out of the model’s hands entirely: Cognito establishes it once at login, the Runtime validates the token and forwards it to the agent, and the agent reads the email from that validated token and injects it into the tool call itself. The identity scoped tool exposes no email parameter to the model, so there is no field a prompt could set to ask for someone else’s data, and the observability trace shows exactly which identity each tool used. MongoDB Atlas plays a supporting role here, the operational database the organization already has, reachable from AWS without a password to manage, thanks to AWS IAM database authentication. This is the point of mixing the two worlds: you do not have to re-platform your data onto an agent runtime to put an agent in front of it. Keep AgentCore as the agent platform, keep Atlas as the governed data source, and connect them with a small number of narrow, reviewable pieces.
References
- Companion code for this article
- AWS Lambda: Working with .zip File Archives for Python
- Amazon Bedrock AgentCore
- AgentCore Gateway
- AgentCore Gateway: AWS Lambda function targets
- AgentCore Runtime
- AgentCore: Configure inbound JWT authorizer
- AgentCore Runtime: Pass custom headers (request header allowlist)
- AgentCore Observability
- LangGraph
- MongoDB Atlas
- MongoDB Atlas: AWS IAM Authentication
- MongoDB Atlas: Configure Database Users
- MongoDB Atlas: Private Endpoints
- MongoDB Atlas: VPC Peering
- MongoDB Atlas Vector Search
- MongoDB MCP Server
- pymongo: MONGODB-AWS Authentication
- pymongo: errors module
- MongoDB: Error Codes Reference
- AWS Well-Architected: Agentic AI Lens, Implement Efficient Tool Invocation Patterns
- MongoDB Compass: Authentication Connection Options
- AWS Lambda: Defining Function Permissions with an Execution Role
- AWS: How to Use Trust Policies with IAM Roles
- AWS IAM: The Confused Deputy Problem
- AWS Lambda: Managing Concurrency
Stay Connected
If you found this article helpful and want to learn more about AWS, cloud architecture, AI, infrastructure as code, and cloud security, follow the author for future content and tutorials:
- LinkedIn: https://www.linkedin.com/in/biagolini
- Medium: https://medium.com/@biagolini
- GitHub: https://github.com/biagolini
- YouTube (PT-BR): https://www.youtube.com/@BiagoliniTech
- AWS Builder Center (PT-BR): https://builder.aws.com/community/@cbiagolini
Stay tuned for more practical walkthroughs and solutions for cloud infrastructure and DevOps challenges.
Happy building on AWS! 🚀☁️










