,

Bedrock Managed Agents in Production: AgentCore and MCP

Row of isolated pods each running a robot agent, depicting Bedrock Managed Agents in production on AgentCore Runtime

Getting a demo agent to run uname -a takes half an hour. Running Bedrock Managed Agents in production, with real users, flaky networks and an auditor asking what the agent did last Tuesday, takes discipline. This is the hardening checklist we would apply before letting the OpenAI-powered harness anywhere near a workload that matters, built on the patterns in the sessions documentation.

Move execution to AgentCore Runtime

A self-hosted exec server on a laptop is perfect for learning and terrible for uptime. For production, use Amazon Bedrock AgentCore Runtime as the execution environment. The reference CDK app provisions the Runtime, its storage and its networking, including a NAT gateway for egress, and the session role setup covers AgentCore Runtime activation.

What you gain: isolated execution per session, no hosts to patch, and the same outbound-only model as self-hosted. What you inherit: real infrastructure with real standing cost. More on that below.

Skills and MCP tools: give the agent hands, carefully

Skills are reusable instructions the agent picks up from the skills folder in its workspace. Use them for the stuff you are tired of pasting into prompts: your deployment conventions, how to read your log format, which directories are off-limits. Version them in Git like code, review changes like code, and ship them with the workspace. A skill is a prompt with a change history, which is exactly what you want when someone asks why the agent started behaving differently on Thursday. Keep each skill small and single-purpose; one giant skill file turns into the same unmaintainable mega-prompt you were trying to escape.

MCP servers expose tools through the Model Context Protocol, so the agent can query a ticket system or a database without you writing bespoke glue for each one. Production rules:

  • Every MCP server is an attack surface. Run only servers you trust, pinned to specific versions.
  • Give each server its own narrowly scoped credentials. A read-only reporting tool does not need write access because it was convenient during the hackathon.
  • Prefer tools that are naturally idempotent or have a dry-run mode. You will be glad later.

One state file per concurrent session

The example scripts track session state in a file. Run two sessions from the same shell with the default and they will cheerfully overwrite each other’s state. Set BMA_STATE_FILE per session so concurrent workers never collide:

#!/usr/bin/env bash
set -euo pipefail
export BMA_REGION=us-west-2
export BMA_ENDPOINT="https://bedrock-mantle.${BMA_REGION}.api.aws"
for job in nightly-report build-triage; do
(
export BMA_STATE_FILE="$PWD/state/${job}.json"
export BMA_WORKSPACE_DIRECTORY="$PWD/workspaces/${job}"
mkdir -p "$(dirname "$BMA_STATE_FILE")" "$BMA_WORKSPACE_DIRECTORY/skills"
./scripts/bma/0.create-session.sh
) &
done
wait
Robot agent plugging tool cables into an adapter hub beside a ledger, illustrating MCP tools and event reconciliation

Treat events as hints and items as truth

The SSE event stream is great for live progress, but connections drop, load balancers time out idle streams, and your consumer will restart mid-turn. Design for it:

  • Parse on SSE boundaries (a blank line ends an event), never on raw chunks.
  • On disconnect, reconnect with backoff.
  • After any reconnect, reconcile against durable items from the last item ID you processed. Items are the ledger; events are the radio commentary.
import json, time, requests, boto3
from botocore.auth import SigV4Auth
from botocore.awsrequest import AWSRequest
REGION = "us-west-2"
BASE = f"https://bedrock-mantle.{REGION}.api.aws"
creds = boto3.Session().get_credentials().get_frozen_credentials()
def signed(method, path):
req = AWSRequest(method=method, url=BASE + path)
SigV4Auth(creds, "bedrock-mantle", REGION).add_auth(req)
return dict(req.headers)
def reconcile(session_id, after=None):
"""Walk durable items forward from a cursor; return new items and the cursor."""
seen = []
while True:
q = f"?limit=100&order=asc" + (f"&after={after}" if after else "")
path = f"/openai/v1/agents/sessions/{session_id}/items{q}"
page = requests.get(BASE + path, headers=signed("GET", path), timeout=30).json()
seen += page["data"]
if page["data"]:
after = page["last_id"]
if not page["has_more"]:
return seen, after
def follow(session_id, cursor=None):
path = f"/openai/v1/agents/sessions/{session_id}/events"
backoff = 1
while True:
try:
with requests.get(BASE + path, headers=signed("GET", path),
stream=True, timeout=(10, 300)) as r:
backoff = 1
for line in r.iter_lines(decode_unicode=True):
if line and line.startswith("data:"):
print("event:", line[5:].strip()[:120])
except requests.RequestException:
time.sleep(backoff); backoff = min(backoff * 2, 30)
items, cursor = reconcile(session_id, cursor)
for item in items:
print("item:", json.dumps(item)[:120])

Credentials expire, so in a long-running worker, refresh them before re-signing rather than caching a frozen set forever.

Idempotent submits, or how not to run a migration twice

A turn submit is a POST. If the network dies after the service accepted it but before you saw the response, a blind retry submits the same work twice. For an agent with shell access, that can mean running the same destructive command twice.

The fix is boring and effective: embed a client-generated request tag in the message text, and before any resubmit, inspect the session and reconcile items. If an item containing your tag already exists, the original submit landed, so do not send it again. Only resubmit when the session is idle and your tag is nowhere in the items.

Do not trust idle to mean success

A session returning to idle means the turn ended, not that it worked. Agents are optimistic narrators; they will happily summarize a failed command as progress if the output looked plausible. Inspect command-execution items and check their exit codes before you mark a job green. A pipeline that treats “the agent stopped talking” as success will eventually report a failed deploy as done.

Same energy for cancellation: posting agent.session.input.cancel stops further work, but it does not undo side effects already executed. If a tool wrote rows or deleted files, compensating actions are your job.

Least privilege and cost hygiene for Bedrock Managed Agents in production

  • Session role: allow inference only on the specific models you approved. Keep it separate from the identities that create sessions and attach executors, as the prerequisites describe.
  • Observability: persist items to your own store per session, log every submit with its request tag, and alert on sessions stuck in_progress or landing in failed.
  • Cost: inference bills at Bedrock rates, but stacks bill while idle. NAT gateways are the usual culprit. Audit regularly:
aws ec2 describe-nat-gateways --region us-west-2 \
--filter Name=state,Values=available \
--query "NatGateways[].{id:NatGatewayId,vpc:VpcId,created:CreateTime}" \
--output table

Tear down non-production stacks on a schedule, and remember that deleting a session does not delete workspace files; they follow their own lifecycle. Finally, it is a preview: pin your client code behind a thin wrapper and watch the AWS announcements for API changes.

At Enkompass (enkompass.net), this is our home turf. We build production agent platforms on Bedrock and AgentCore with least-privilege tool access, durable audit trails and automated teardown of idle infrastructure. If you’d rather have seasoned cloud people handle it instead of learning the hard way at 2 AM, let’s talk. Reach out through our contact page or call us directly at +1-828-436-5667 — we’ll help you turn this from a someday-project into a done-deal.

Ready when you are

Let’s map your route to AWS

A free 60-minute architecture review with a senior engineer. No slides, no SDR, just a plan.