writing /

Building Company-Aware AI Agents on AWS Bedrock with AgentCore and MCP

In my post on building an LLM pipeline with SageMaker, we trained and served a model. But a served model just answers what you send it — it has no hands. It can't look up an order, query your internal wiki, or open a ticket. Agentic AI is the layer that gives a model those hands: tools it can call, documents it can read, and a loop that decides when to do which.

On AWS, the clean managed path for this is Bedrock AgentCore paired with the Model Context Protocol (MCP). In this post I'll build an agent that connects to my company's internal tools over MCP, grounds its answers in internal documents through a Bedrock Knowledge Base, and ships as a small chatbot you can run in a terminal. Let me walk you through it.

From Models to Agents

An "agent" is three things wired together:

  1. A model to reason (here, Claude on Bedrock)
  2. A set of tools it can call (your company's APIs, exposed as MCP tools)
  3. A loop that lets the model decide: answer directly, call a tool, read a doc, then continue

The hard parts have always been hosting that loop reliably and connecting it to real company systems without hand-rolling an integration per API. That's exactly what AgentCore handles. AgentCore Runtime hosts the agent as a managed, serverless endpoint. AgentCore Gateway turns your existing APIs into MCP tools the agent can discover and call. And a Bedrock Knowledge Base gives it retrieval over your documents.

The Architecture at a Glance

Here's how the pieces fit together:

  1. Bedrock Knowledge Base indexes internal docs sitting in S3 (managed RAG)
  2. AgentCore Gateway exposes company APIs (order lookup, ticketing, etc.) as MCP tools
  3. The agent — a model plus a tool-use loop — connects to the Gateway over MCP and to the Knowledge Base for retrieval
  4. AgentCore Runtime hosts that agent behind a stable, session-aware endpoint
  5. A CLI chatbot invokes the endpoint and streams responses back

Each layer has one job and can be built and tested on its own. Let's go through them.

Grounding the Agent in Internal Docs

Before the agent can answer questions about internal policies, runbooks, or specs, it needs to read them. A Bedrock Knowledge Base handles the whole RAG pipeline for you: point it at an S3 bucket of documents, pick an embeddings model, and Bedrock chunks, embeds, and indexes everything into a vector store.

Once the Knowledge Base is synced, you can query it directly with retrieve_and_generate — this both retrieves the relevant chunks and generates a grounded answer in one call:

import boto3

agent_runtime = boto3.client("bedrock-agent-runtime")

response = agent_runtime.retrieve_and_generate(
    input={"text": "What's our on-call escalation policy for Sev-1 incidents?"},
    retrieveAndGenerateConfiguration={
        "type": "KNOWLEDGE_BASE",
        "knowledgeBaseConfiguration": {
            "knowledgeBaseId": "KB1234567890",
            "modelArn": "arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-3-5-sonnet-20241022-v2:0",
        },
    },
)

print(response["output"]["text"])

That's a useful building block on its own. But we don't want a separate call path — we want the agent to reach for the docs on its own when a question calls for it. We'll wire the Knowledge Base in as a tool in a moment.

Exposing Company Tools Through the MCP Gateway

This is the heart of it. Your company already has APIs — an order service, a ticketing system, a customer database. AgentCore Gateway takes those and exposes them as MCP tools, so any MCP-speaking agent can discover and call them through one standardized endpoint. No per-API glue code in the agent itself.

First, create the gateway. It speaks MCP and authorizes inbound callers with a JWT (from Cognito or any OIDC provider):

import boto3

gateway_client = boto3.client("bedrock-agentcore-control")

auth_config = {
    "customJWTAuthorizer": {
        "allowedClients": ["<cognito_client_id>"],
        "discoveryUrl": "<cognito_oidc_discovery_url>",
    }
}

gateway = gateway_client.create_gateway(
    name="CompanyToolsGateway",
    roleArn="arn:aws:iam::123456789012:role/AgentCoreGatewayRole",
    protocolType="MCP",
    authorizerType="CUSTOM_JWT",
    authorizerConfiguration=auth_config,
    description="Internal company tools exposed as MCP",
)

gateway_id = gateway["gatewayId"]
gateway_url = gateway["gatewayUrl"]

Then add targets — each target is a backend the gateway turns into tools. The simplest is a Lambda function, where you declare the tool schema inline. Here's an "order status" tool backed by a Lambda:

lambda_target_config = {
    "mcp": {
        "lambda": {
            "lambdaArn": "arn:aws:lambda:us-east-1:123456789012:function:get-order-status",
            "toolSchema": {
                "inlinePayload": [
                    {
                        "name": "get_order_status",
                        "description": "Look up the current status of a customer order by ID",
                        "inputSchema": {
                            "type": "object",
                            "properties": {"orderId": {"type": "string"}},
                            "required": ["orderId"],
                        },
                    }
                ]
            },
        }
    }
}

gateway_client.create_gateway_target(
    gatewayIdentifier=gateway_id,
    name="OrderStatusTool",
    description="Order status lookup",
    targetConfiguration=lambda_target_config,
    credentialProviderConfigurations=[{"credentialProviderType": "GATEWAY_IAM_ROLE"}],
)

If your service already has an OpenAPI spec, you can skip the inline schema entirely — point the target at the spec in S3 and the gateway generates a tool per operation:

openapi_target_config = {
    "mcp": {"openApiSchema": {"s3": {"uri": "s3://my-bucket/specs/ticketing.yaml"}}}
}

gateway_client.create_gateway_target(
    gatewayIdentifier=gateway_id,
    name="TicketingTools",
    description="Ticketing API from OpenAPI spec",
    targetConfiguration=openapi_target_config,
    credentialProviderConfigurations=[
        {
            "credentialProviderType": "API_KEY",
            "credentialProvider": {
                "apiKeyCredentialProvider": {
                    "credentialParameterName": "api_key",
                    "providerArn": "<api-key-credential-provider-arn>",
                    "credentialLocation": "HEADER",
                }
            },
        }
    ],
)

When you create a target, the gateway immediately calls the MCP tools/list capability behind the scenes and indexes the tools, so they're discoverable right away. On the outbound side, the gateway handles auth to your backend for you — IAM (SigV4), OAuth, or an API key — so your credentials never live in the agent.

Assembling the Agent

Now we bring the model, the tools, and the docs together. I'll use the Strands Agents SDK for the agent loop — it's what AgentCore's toolkit is built around — and wrap it in BedrockAgentCoreApp so it can be deployed to the Runtime.

The agent connects to the Gateway as an MCP client: it opens a streamable HTTP connection to the gateway URL with a bearer token, lists the available tools, and hands them to the model. For documents, Strands ships a built-in retrieve tool that queries a Bedrock Knowledge Base (it reads the KNOWLEDGE_BASE_ID environment variable), so the model can pull from internal docs the same way it calls any other tool.

import os

from bedrock_agentcore import BedrockAgentCoreApp
from strands import Agent
from strands.models import BedrockModel
from strands_tools import retrieve
from strands.tools.mcp import MCPClient
from mcp.client.streamable_http import streamablehttp_client

app = BedrockAgentCoreApp()

GATEWAY_URL = os.environ["GATEWAY_URL"]
MODEL_ID = "anthropic.claude-3-5-sonnet-20241022-v2:0"

# The Strands `retrieve` tool reads this to know which Knowledge Base to query.
os.environ.setdefault("KNOWLEDGE_BASE_ID", "KB1234567890")

model = BedrockModel(model_id=MODEL_ID)


def gateway_transport():
    token = get_gateway_token()  # fetch an OAuth token for the gateway's JWT authorizer
    return streamablehttp_client(
        GATEWAY_URL, headers={"Authorization": f"Bearer {token}"}
    )


mcp_client = MCPClient(gateway_transport)

SYSTEM_PROMPT = (
    "You are a helpful internal assistant. Use the company tools to look up live "
    "data, and use the retrieve tool to ground answers in internal documentation. "
    "Cite what you looked up."
)


@app.entrypoint
def invoke(payload):
    user_message = payload.get("prompt", "")
    with mcp_client:
        tools = mcp_client.list_tools_sync() + [retrieve]
        agent = Agent(model=model, system_prompt=SYSTEM_PROMPT, tools=tools)
        result = agent(user_message)
    return {"result": result.message}


if __name__ == "__main__":
    app.run()

That's the whole agent. The @app.entrypoint decorator is what makes this deployable — BedrockAgentCoreApp wraps your function in the HTTP contract AgentCore Runtime expects, so you never touch the server plumbing. Inside, the model now has the company's MCP tools and the Knowledge Base at its disposal, and the loop decides which to reach for.

Deploying to AgentCore Runtime

With the agent written, the starter toolkit deploys it in two commands. configure inspects your entrypoint and generates the runtime config; launch builds the container, pushes it, and stands up the managed endpoint:

pip install bedrock-agentcore bedrock-agentcore-starter-toolkit strands-agents strands-agents-tools

agentcore configure -e agent.py
agentcore launch

When launch finishes, it prints the agent runtime ARN. That ARN is the stable address other services — including our chatbot — use to invoke the agent. AgentCore handles scaling, session isolation, and keeping the endpoint warm.

You can smoke-test it right from the CLI:

agentcore invoke '{"prompt": "What is the status of order 4815 and what does our returns policy say?"}'

If the tools and Knowledge Base are wired correctly, the agent will call get_order_status for the live data and retrieve for the policy, then combine both into one answer.

A Quick Chatbot

The payoff: a small terminal chatbot that talks to the deployed agent. The key detail is the session id — reuse the same runtimeSessionId across turns and AgentCore keeps the conversation context, so the agent remembers what you said earlier.

import json
import uuid

import boto3

AGENT_ARN = "arn:aws:bedrock-agentcore:us-east-1:123456789012:runtime/company-assistant-XXXX"

client = boto3.client("bedrock-agentcore")
session_id = str(uuid.uuid4())  # one id for the whole conversation

print("Company assistant ready. Ctrl-C to quit.\n")

while True:
    try:
        prompt = input("you › ").strip()
    except (EOFError, KeyboardInterrupt):
        print("\nbye")
        break

    if not prompt:
        continue

    response = client.invoke_agent_runtime(
        agentRuntimeArn=AGENT_ARN,
        runtimeSessionId=session_id,
        payload=json.dumps({"prompt": prompt}).encode(),
        qualifier="DEFAULT",
    )

    # The response streams back as byte chunks — collect and decode them.
    chunks = [chunk.decode("utf-8") for chunk in response.get("response", [])]
    answer = json.loads("".join(chunks))
    print(f"bot › {answer['result']}\n")

Run it, and you've got a working internal assistant:

you › what's the status of order 4815?
bot › Order 4815 shipped yesterday and is out for delivery, expected today.

you › and if it arrives damaged, what should the customer do?
bot › Per our returns policy, damaged items can be reported within 30 days for a
      full refund or replacement — here's the process...

The first answer came from the MCP order tool; the second came from the Knowledge Base. The agent decided which to use, and the session id tied the two turns together.

Putting It All Together

Step back and the whole thing is just a few well-bounded layers:

  • A Knowledge Base grounds the agent in internal documents
  • A Gateway exposes company APIs as MCP tools, handling auth on both sides
  • The agent wires the model to those tools and docs with a decision loop
  • AgentCore Runtime hosts it serverlessly behind a session-aware endpoint
  • A CLI chatbot consumes it like any other API

The nice part is that none of this required managing a server, and each layer is swappable — add a new company API by creating one more gateway target, and every agent connected to the gateway can use it immediately. No redeploys, no glue code.

If you've already got models running on Bedrock, this is how you give them hands. Next time someone asks your assistant a question that spans a live system and your internal docs, it'll just handle it.