I built a customer support agent on Amazon Bedrock AgentCore β€” and it refused to say my name A developer built a customer support agent on Amazon Bedrock AgentCore that combines an MCP Gateway, a Bedrock Knowledge Base, AgentCore Memory, Code Interpreter, and a Browser Tool, reasoning with Amazon Nova 2 Lite via the Strands Agents SDK. The writeup documents three implementation gotchas, including that Gateway target names reject underscores and must use hyphens only, and one bug that passed every test. I spent ten years in asset management, most of it translating what portfolio managers and quants built into something clients could actually follow. Now I build agents. It turns out the job is weirdly similar: an agent is mostly a translation layer between a person asking a vague question and a set of systems that only understand precise ones. This is the second project of the AWS AI & ML Scholars β€” Future Agent Engineer track: a customer support agent for a fictional e-commerce store, running on Amazon Bedrock AgentCore . Here's how it's put together, the three things that tripped me up, and one bug that passed every test. πŸ‘‰ Repo: github.com/Mialy333/agentcore-customer-support-agent https://github.com/Mialy333/agentcore-customer-support-agent One conversational entrypoint, six capabilities: | Ask | What answers it | |---|---| | "Where's my order ORD-001?" | AgentCore Gateway MCP β†’ API Gateway β†’ Lambda | | "Refund my Kindle" | AgentCore Gateway MCP β†’ Lambda | | "What does Platinum tier get me?" | Bedrock Knowledge Base RAG | | "Do you remember me?" | AgentCore Memory via a Strands hook | | "What's my discount with 4,250 points?" | Code Interpreter sandboxed Python | | "What's on this web page?" | Browser Tool | It runs on AgentCore Runtime serverless, direct code deploy, Python 3.13 , uses the Strands Agents SDK , and reasons with Amazon Nova 2 Lite . The whole agent fits in a single main.py . The entrypoint is short because each capability is a tool or a hook: python @app.entrypoint async def invoke payload, context=None : user input = payload.get "prompt", "" actor id = payload.get "customer id", "anonymous" session id = payload.get "session id" or str uuid.uuid4 memory hook = MemoryHook actor id, session id, memory client, MEMORY ID browser = AgentCoreBrowser region=REGION tools = search knowledge base, calculate loyalty discount, browser.browser gateway client = MCPClient lambda: streamable http client GATEWAY URL with gateway client: all tools = tools + gateway client.list tools sync agent = Agent model=model, tools=all tools, hooks= memory hook , system prompt=SYSTEM PROMPT return agent user input .message "content" 0 "text" Nine tools reach the model: three local ones, plus six discovered at runtime from the Gateway over MCP. The agent doesn't know or care that get order lives behind API Gateway while initiate refund is a bare Lambda β€” it just sees an MCP tool list. This was the part I found most "production-shaped". The two Lambdas have nothing in common: order-tracker event "resource" and pathParameters . refund-processor raw tool = context.client context.custom.get "bedrockAgentCoreToolName", "" tool = raw tool.split " ", 1 -1 "refund-processor initiate refund" β†’ "initiate refund" AgentCore Gateway wraps both as targets of one MCP server. For the REST API, each operation ID get order , get customer , get customer orders becomes a tool name. For the Lambda, you paste a JSON tool schema. Gotcha 1 β€” target names reject underscores. I named my targets order tracker and refund processor , matching the Lambda files. The Gateway was created fine… and then both targets failed: the name must match 0-9a-zA-Z - ? {1,100} . Hyphens only. order-tracker and refund-processor went through. Before writing any agent code, npx @modelcontextprotocol/inspector against the Gateway URL is the fastest way to confirm all six tools are actually exposed. The knowledge base is a single product catalog.txt products, return windows, refund timelines, loyalty tiers uploaded to S3 and synced into a Managed Knowledge Base . The tool is a thin wrapper around the Retrieve API: resp = bedrock runtime.retrieve knowledgeBaseId=KB ID, retrievalQuery={"text": query}, chunks = r "content" "text" for r in resp.get "retrievalResults", if r.get "content", {} .get "text" return "\n---\n".join chunks Asked about the Platinum tier, the agent returned exactly what the catalog says β€” free same-day shipping, 15% discount, priority support β€” instead of whatever the model half-remembers about loyalty programs in general. That's the whole point of RAG: answers you can trace back to a document. AgentCore Memory is configured with two built-in strategies: cs agent/{actorId}/facts cs agent/{actorId}/preferences The agent saves raw conversation events; AgentCore extracts durable facts and preferences from them in the background. On the agent side, I used a Strands HookProvider with two callbacks: python def register hooks self, registry : registry.add callback MessageAddedEvent, self.retrieve customer context registry.add callback AfterInvocationEvent, self.save support interaction Customer Context: block to that message. create event . The design choice I like: the agent's reasoning loop never knows memory exists. It just receives a slightly richer message. Retrieval and saving each live in their own method, and tool-result messages are skipped so memory is only queried on real user turns. The test: in session A, "Hi, I am Jane. I prefer concise responses." Wait 30 seconds for extraction. In session B, "Do you remember my name and communication preference?" First run: it recalled "prefers concise responses"… and then declined to tell me my name, explaining it didn't have access to personal identification details for security reasons. Nothing in my code blocked it. The memory record was right there in the context. The model itself decided a name looked like sensitive data and got cautious β€” about a name the customer had volunteered minutes earlier. The fix was a few sentences in the system prompt: the Customer Context block contains facts the customer previously and voluntarily shared, and stating them back is expected behavior. After that: It remembered Jane, the preference, and her two recent orders. Lesson: guardrails baked into a foundation model can misfire in directions you never asked for, and sometimes prompt engineering means removing caution rather than adding it. Loyalty math has just enough rules to go wrong: points redeem in 500-point blocks $5 each , capped at 50% of the order, then a tier discount Gold 10%, Platinum 15% applies, then you earn points on what you paid. Instead of trusting the model, calculate loyalty discount generates a small Python program and runs it in the AgentCore Code Interpreter sandbox: with code session REGION as session: response = session.invoke "executeCode", {"code": code, "language": "python", "clearContext": True} Gold member, 4,250 points, $150 order: If the sandbox is unavailable, the tool falls back to a tier-only calculation and says so in its output, rather than failing silently or letting the model guess. browser = AgentCoreBrowser region=REGION tools = ..., browser.browser AgentCore runs a managed headless browser; the agent got the live page title from udacity.com. The only real work was IAM. The course intentionally uses the Python Bedrock AgentCore Starter Toolkit : uv run agentcore configure --entrypoint main.py --name customer support agent \ --deployment-type direct code deploy --runtime PYTHON 3 13 --disable-memory uv run agentcore deploy uv run setup permissions.py setup permissions.py reads the resource IDs from main.py as text with ast , never importing the agent , checks the account and role match the deployment, then attaches one inline policy scoped to this Knowledge Base, this Memory and the managed browser. Least privilege, rerunnable. Gotcha 3 β€” region drift. One configure run silently picked up us-west-2 while every resource lived in us-east-1 . Read the configuration summary before you deploy. Side note: every invoke now prints a notice that the Starter Toolkit is no longer supported in favor of the npm @aws/agentcore CLI. Don't install both β€” they ship the same agentcore binary. All six scenarios passed. Then I re-read the refund output more carefully: Refund ID βœ…, APPROVED βœ…, "3–5 business days" βœ…β€¦ and Refund Amount: $0 . Plus a confident "you don't need to return anything, this was a digital product" β€” about a physical Kindle. What happened: amount is optional in the refund tool's schema, the Lambda defaults it to 0, and the model never called get order to look up the $139.99 total first. Then it filled the gap with a plausible story. The test checked for the shape of the right answer, not for the answer being right. Fixes I'd ship in v2: amount required in the schema, or better, have the Lambda look the amount up server-side so the model can't get it wrong; This is the kind of thing I'd rather find in a sandbox than in a customer's bank account. agentcore destroy , then Gateway, Memory, KB, vector store, S3, API Gateway, Lambdas . The hardest part of AgentCore isn't the Python. It's getting Gateway, Memory, the Knowledge Base, the Code Interpreter and the Browser to agree with each other β€” same region, same protocol, same IAM role. And the most interesting failures weren't infrastructure failures. They were the model being too careful with a name, and not careful enough with a refund amount. Which is, honestly, the same lesson I learned explaining portfolios to clients: the risk is rarely in the system you can see; it's in the gap between what someone asked and what got executed. The full code, architecture diagrams, all six test screenshots and a 43-step setup walkthrough are in the repo: πŸ‘‰ github.com/Mialy333/agentcore-customer-support-agent https://github.com/Mialy333/agentcore-customer-support-agent I'm Mialy, building at the intersection of AI agents and digital assets as @ellebuild. If you've hit other AgentCore gotchas, I'd love to hear them in the comments.