From 46c18c6d72da4a4c822e60bc8d8b5166c75e3f91 Mon Sep 17 00:00:00 2001 From: Shreya Pawaskar Date: Mon, 29 Jun 2026 11:52:22 -0700 Subject: [PATCH] Add FMKB-via-AgentCore-Gateway sample deployed to AgentCore Runtime Adds a sibling to 01-end-to-end-example-with-ac-gateway. The 01 folder runs the Strands agent locally; this folder packages the same agent and deploys it to AgentCore Runtime via the agentcore CLI (bedrock-agentcore-starter-toolkit). The sample is split into two scripted sub-folders so each piece can be exercised independently: - 01-raw-mcp/ provisions the gateway role + gateway + KB target, exercises the gateway -> KB plumbing without an agent in the loop, and tears it all down. Idempotent: re-running setup reuses the same resources by exact-name match instead of orphaning siblings. - 02-strands-agent/ is the BedrockAgentCoreApp entrypoint, deployed via agentcore configure && agentcore deploy. Includes the runtime trust policy and a least-privilege execution policy that grants bedrock-agentcore:InvokeGateway scoped to the gateway ARN. --- .../.env.example | 23 +++ .../02-fmkb-with-agentcore-runtime/.gitignore | 8 + .../01-raw-mcp/README.md | 23 +++ .../01-raw-mcp/cleanup.py | 88 +++++++++++ .../01-raw-mcp/raw_mcp_call.py | 76 ++++++++++ .../01-raw-mcp/setup_gateway.py | 127 ++++++++++++++++ .../02-strands-agent/README.md | 143 ++++++++++++++++++ .../02-strands-agent/fmkb_gateway_strands.py | 71 +++++++++ .../iam/runtime-execution-policy.json | 92 +++++++++++ .../iam/runtime-trust-policy.json | 21 +++ .../02-strands-agent/requirements.txt | 10 ++ .../02-fmkb-with-agentcore-runtime/README.md | 104 +++++++++++++ .../requirements.txt | 10 ++ .../utils/__init__.py | 0 .../utils/gateway.py | 131 ++++++++++++++++ .../utils/managed_kb.py | 29 ++++ 16 files changed, 956 insertions(+) create mode 100644 rag/managed-knowledge-bases/03-use-case-example/02-fmkb-with-agentcore-runtime/.env.example create mode 100644 rag/managed-knowledge-bases/03-use-case-example/02-fmkb-with-agentcore-runtime/.gitignore create mode 100644 rag/managed-knowledge-bases/03-use-case-example/02-fmkb-with-agentcore-runtime/01-raw-mcp/README.md create mode 100644 rag/managed-knowledge-bases/03-use-case-example/02-fmkb-with-agentcore-runtime/01-raw-mcp/cleanup.py create mode 100644 rag/managed-knowledge-bases/03-use-case-example/02-fmkb-with-agentcore-runtime/01-raw-mcp/raw_mcp_call.py create mode 100644 rag/managed-knowledge-bases/03-use-case-example/02-fmkb-with-agentcore-runtime/01-raw-mcp/setup_gateway.py create mode 100644 rag/managed-knowledge-bases/03-use-case-example/02-fmkb-with-agentcore-runtime/02-strands-agent/README.md create mode 100644 rag/managed-knowledge-bases/03-use-case-example/02-fmkb-with-agentcore-runtime/02-strands-agent/fmkb_gateway_strands.py create mode 100644 rag/managed-knowledge-bases/03-use-case-example/02-fmkb-with-agentcore-runtime/02-strands-agent/iam/runtime-execution-policy.json create mode 100644 rag/managed-knowledge-bases/03-use-case-example/02-fmkb-with-agentcore-runtime/02-strands-agent/iam/runtime-trust-policy.json create mode 100644 rag/managed-knowledge-bases/03-use-case-example/02-fmkb-with-agentcore-runtime/02-strands-agent/requirements.txt create mode 100644 rag/managed-knowledge-bases/03-use-case-example/02-fmkb-with-agentcore-runtime/README.md create mode 100644 rag/managed-knowledge-bases/03-use-case-example/02-fmkb-with-agentcore-runtime/requirements.txt create mode 100644 rag/managed-knowledge-bases/03-use-case-example/02-fmkb-with-agentcore-runtime/utils/__init__.py create mode 100644 rag/managed-knowledge-bases/03-use-case-example/02-fmkb-with-agentcore-runtime/utils/gateway.py create mode 100644 rag/managed-knowledge-bases/03-use-case-example/02-fmkb-with-agentcore-runtime/utils/managed_kb.py diff --git a/rag/managed-knowledge-bases/03-use-case-example/02-fmkb-with-agentcore-runtime/.env.example b/rag/managed-knowledge-bases/03-use-case-example/02-fmkb-with-agentcore-runtime/.env.example new file mode 100644 index 000000000..e95f0d4e7 --- /dev/null +++ b/rag/managed-knowledge-bases/03-use-case-example/02-fmkb-with-agentcore-runtime/.env.example @@ -0,0 +1,23 @@ +# Example environment for the Managed KB + AgentCore Gateway runtime example. +# +# The real file (.env.fmkb-gateway) is AUTO-GENERATED by 01-raw-mcp/setup_gateway.py +# once the gateway + KB target are created — you do not write it by hand. +# It is gitignored; never commit the populated version (it contains your account ID). +# This file exists only to document the variables the sample produces and consumes. + +# --- Written by 01-raw-mcp/setup_gateway.py, sourced before deploy --- +export REGION=us-west-2 +export ACCOUNT_ID=111122223333 +export KB_ID=XXXXXXXXXX +export GATEWAY_ID=fmkb-sample-gateway-xxxxxxxxxx +export GATEWAY_URL=https://fmkb-sample-gateway-xxxxxxxxxx.gateway.bedrock-agentcore.us-west-2.amazonaws.com/mcp +export TARGET_ID=XXXXXXXXXX +export TARGET_NAME=fmkb-sample-target +export GATEWAY_ROLE_NAME=fmkb-sample-gateway-role + +# --- Optional overrides read by 02-strands-agent/fmkb_gateway_strands.py --- +# GATEWAY_URL is required at runtime (set above / injected into the runtime env). +# export AWS_REGION=us-west-2 +# export MODEL_ID=us.anthropic.claude-sonnet-4-5-20250929-v1:0 +# export SYSTEM_PROMPT="You are a knowledge-base assistant. Answer ONLY using the KB tool..." +# export LOG_LEVEL=INFO diff --git a/rag/managed-knowledge-bases/03-use-case-example/02-fmkb-with-agentcore-runtime/.gitignore b/rag/managed-knowledge-bases/03-use-case-example/02-fmkb-with-agentcore-runtime/.gitignore new file mode 100644 index 000000000..a6334a37d --- /dev/null +++ b/rag/managed-knowledge-bases/03-use-case-example/02-fmkb-with-agentcore-runtime/.gitignore @@ -0,0 +1,8 @@ +.env +.env.fmkb-gateway +.bedrock_agentcore.yaml +.bedrock_agentcore/ +__pycache__/ +*.pyc +.venv/ +.pytest_cache/ diff --git a/rag/managed-knowledge-bases/03-use-case-example/02-fmkb-with-agentcore-runtime/01-raw-mcp/README.md b/rag/managed-knowledge-bases/03-use-case-example/02-fmkb-with-agentcore-runtime/01-raw-mcp/README.md new file mode 100644 index 000000000..a3f8fff72 --- /dev/null +++ b/rag/managed-knowledge-bases/03-use-case-example/02-fmkb-with-agentcore-runtime/01-raw-mcp/README.md @@ -0,0 +1,23 @@ +# 01-raw-mcp — verify the gateway path without an agent + +The smallest possible test of the FMKB-as-MCP-tool plumbing. Run this first when something's broken — it isolates the gateway/IAM layer from the agent layer. + +## What it does + +1. **`setup_gateway.py`** — creates an IAM role for the gateway, an AgentCore Gateway, and a KB target pointing at the Managed KB you supply with `--kb-id`. Writes the resource ids to `.env.fmkb-gateway` in the repo root. +2. **`raw_mcp_call.py`** — opens an MCP session against the gateway URL using SigV4-signed streamable HTTP, lists tools (one per gateway target), and calls the `___Retrieve` tool with the prompt you pass on the CLI. Prints the raw retrieval results. +3. **`cleanup.py`** — deletes the target, the gateway, and the gateway role. The KB itself is *not* touched. + +## Use it + +```bash +pip install -r ../requirements.txt +python setup_gateway.py --kb-id --region us-west-2 +source ../.env.fmkb-gateway + +python raw_mcp_call.py "What does the knowledge base say about cat food?" + +python cleanup.py +``` + +Required IAM on your caller principal: ability to create/get/delete IAM roles in the `bedrock-agentcore-*` prefix, plus AgentCore control-plane access. See [Use the AgentCore CLI](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/runtime-permissions.html#runtime-permissions-cli) for the full policy. diff --git a/rag/managed-knowledge-bases/03-use-case-example/02-fmkb-with-agentcore-runtime/01-raw-mcp/cleanup.py b/rag/managed-knowledge-bases/03-use-case-example/02-fmkb-with-agentcore-runtime/01-raw-mcp/cleanup.py new file mode 100644 index 000000000..9238596c7 --- /dev/null +++ b/rag/managed-knowledge-bases/03-use-case-example/02-fmkb-with-agentcore-runtime/01-raw-mcp/cleanup.py @@ -0,0 +1,88 @@ +"""Tear down the gateway, KB target, and gateway IAM role created by setup_gateway.py. + +Reads ../.env.fmkb-gateway. Does NOT delete the KB. +""" +from __future__ import annotations + +import os +import pathlib +import sys +import time + +import boto3 +from botocore.exceptions import ClientError + +ROOT = pathlib.Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT)) + +from utils import gateway as gw # noqa: E402 + + +def load_env() -> None: + env = ROOT / ".env.fmkb-gateway" + if not env.exists(): + sys.exit(f"{env} not found; nothing to clean up") + for line in env.read_text().splitlines(): + line = line.strip() + if not line or line.startswith("#"): + continue + if line.startswith("export "): + line = line[len("export "):] + if "=" not in line: + continue + k, v = line.split("=", 1) + os.environ[k.strip()] = v.strip() + + +def main() -> int: + load_env() + region = os.environ["REGION"] + gateway_id = os.environ["GATEWAY_ID"] + target_id = os.environ.get("TARGET_ID") + role_name = os.environ.get("GATEWAY_ROLE_NAME") + + iam = boto3.client("iam") + control = boto3.client("bedrock-agentcore-control", region_name=region) + + if target_id: + try: + gw.delete_target(gateway_id, target_id, region) + print(f"deleted target {target_id}") + except ClientError as e: + print(f"delete_gateway_target: {e}") + + # Gateway deletion is rejected while any target is still being torn down, + # so wait for the target list to drain before retrying. + for _ in range(12): + try: + remaining = control.list_gateway_targets( + gatewayIdentifier=gateway_id + ).get("items", []) + except ClientError: + remaining = [] + if not remaining: + break + time.sleep(5) + try: + gw.delete_gateway(gateway_id, region) + print(f"deleted gateway {gateway_id}") + except ClientError as e: + print(f"delete_gateway: {e}") + + if role_name: + try: + for p in iam.list_role_policies(RoleName=role_name)["PolicyNames"]: + iam.delete_role_policy(RoleName=role_name, PolicyName=p) + iam.delete_role(RoleName=role_name) + print(f"deleted role {role_name}") + except ClientError as e: + print(f"delete_role: {e}") + + env = ROOT / ".env.fmkb-gateway" + env.unlink(missing_ok=True) + print(f"removed {env}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/rag/managed-knowledge-bases/03-use-case-example/02-fmkb-with-agentcore-runtime/01-raw-mcp/raw_mcp_call.py b/rag/managed-knowledge-bases/03-use-case-example/02-fmkb-with-agentcore-runtime/01-raw-mcp/raw_mcp_call.py new file mode 100644 index 000000000..987798ba7 --- /dev/null +++ b/rag/managed-knowledge-bases/03-use-case-example/02-fmkb-with-agentcore-runtime/01-raw-mcp/raw_mcp_call.py @@ -0,0 +1,76 @@ +"""Call the FMKB Retrieve tool through the gateway via raw MCP+SigV4. + +No agent in the loop. Useful as a smoke test before deploying anything to Runtime. +""" +from __future__ import annotations + +import argparse +import asyncio +import json +import os +import sys + +from mcp.client.session import ClientSession +from mcp_proxy_for_aws.client import aws_iam_streamablehttp_client + + +async def run(prompt: str, gateway_url: str, region: str, target_name: str) -> int: + tool_name = f"{target_name}___Retrieve" + async with aws_iam_streamablehttp_client( + endpoint=gateway_url, + aws_service="bedrock-agentcore", + aws_region=region, + ) as (read, write, _): + async with ClientSession(read, write) as session: + await session.initialize() + tools = await session.list_tools() + names = [t.name for t in tools.tools] + if tool_name not in names: + print(f"tool {tool_name!r} not found. Available: {names}", file=sys.stderr) + return 2 + result = await session.call_tool( + tool_name, {"retrievalQuery": {"text": prompt}} + ) + for c in result.content: + if hasattr(c, "text"): + try: + parsed = json.loads(c.text) + except json.JSONDecodeError: + print(c.text) + continue + for hit in parsed.get("retrievalResults", [])[:5]: + score = hit.get("score") + score_str = f"{score:.4f}" if isinstance(score, (int, float)) else " n/a" + content = hit.get("content") or {} + # `content` is a tagged union — only TEXT chunks have `.text`. + if content.get("type") == "TEXT" or "text" in content: + text = (content.get("text") or "")[:300].replace("\n", " ") + else: + text = f"<{content.get('type','UNKNOWN')} chunk; not previewable>" + loc = hit.get("location") or {} + print(f" score={score_str} {text}…") + if loc: + print(f" from: {json.dumps(loc)[:200]}") + return 0 + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("prompt") + args = parser.parse_args() + + missing = [k for k in ("GATEWAY_URL", "REGION", "TARGET_NAME") + if not os.environ.get(k)] + if missing: + sys.exit(f"missing env vars {missing}; did you `source .env.fmkb-gateway`?") + + return asyncio.run(run( + prompt=args.prompt, + gateway_url=os.environ["GATEWAY_URL"], + region=os.environ["REGION"], + target_name=os.environ["TARGET_NAME"], + )) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/rag/managed-knowledge-bases/03-use-case-example/02-fmkb-with-agentcore-runtime/01-raw-mcp/setup_gateway.py b/rag/managed-knowledge-bases/03-use-case-example/02-fmkb-with-agentcore-runtime/01-raw-mcp/setup_gateway.py new file mode 100644 index 000000000..4ec6600b9 --- /dev/null +++ b/rag/managed-knowledge-bases/03-use-case-example/02-fmkb-with-agentcore-runtime/01-raw-mcp/setup_gateway.py @@ -0,0 +1,127 @@ +"""Provision an IAM gateway role + an AgentCore Gateway + an FMKB Retrieve target. + +Writes resource ids to ../.env.fmkb-gateway so 02-strands-agent can pick them up. + +Idempotency: re-running with the same --name-prefix reuses the existing gateway +role, gateway, and KB target by exact-name match — no orphans on re-run. +Names are deterministic (no random suffix) so reuse actually triggers. +""" +from __future__ import annotations + +import argparse +import json +import pathlib +import sys +import time + +import boto3 +from botocore.exceptions import ClientError + +ROOT = pathlib.Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT)) + +from utils.managed_kb import assert_kb_active # noqa: E402 +from utils import gateway as gw # noqa: E402 + + +def get_or_create_gateway_role(name: str, account_id: str, region: str, + kb_id: str) -> str: + iam = boto3.client("iam") + trust = json.dumps(gw.gateway_role_trust_policy(account_id, region)) + perms = json.dumps( + gw.gateway_role_permission_policy(account_id, region, kb_id=kb_id) + ) + try: + iam.create_role( + RoleName=name, + AssumeRolePolicyDocument=trust, + Description="AgentCore Gateway role for FMKB Retrieve", + ) + print(f"created role {name}") + except ClientError as e: + if e.response["Error"]["Code"] != "EntityAlreadyExists": + raise + iam.update_assume_role_policy(RoleName=name, PolicyDocument=trust) + print(f"role {name} exists; refreshed trust policy") + iam.put_role_policy( + RoleName=name, PolicyName=f"{name}-inline", PolicyDocument=perms, + ) + role_arn = iam.get_role(RoleName=name)["Role"]["Arn"] + # IAM is eventually consistent; AgentCore will reject the role if it tries to + # assume it before it's visible. A short wait avoids "role not found" on first deploy. + time.sleep(8) + return role_arn + + +def get_or_create_gateway(name: str, role_arn: str, region: str) -> dict: + control = boto3.client("bedrock-agentcore-control", region_name=region) + for page in control.get_paginator("list_gateways").paginate(): + for it in page["items"]: + if it["name"] == name and it["status"] == "READY": + full = control.get_gateway(gatewayIdentifier=it["gatewayId"]) + print(f"reusing gateway {full['gatewayId']}") + return full + print(f"creating gateway {name}…") + return gw.create_gateway(name=name, role_arn=role_arn, region=region) + + +def get_or_create_target(gateway_id: str, name: str, kb_id: str, region: str) -> dict: + control = boto3.client("bedrock-agentcore-control", region_name=region) + for it in control.list_gateway_targets(gatewayIdentifier=gateway_id).get("items", []): + if it["name"] == name and it["status"] == "READY": + print(f"reusing target {it['targetId']}") + return control.get_gateway_target( + gatewayIdentifier=gateway_id, targetId=it["targetId"] + ) + print(f"creating target {name}…") + return gw.create_kb_target( + gateway_id=gateway_id, kb_id=kb_id, name=name, region=region, + ) + + +def main() -> int: + p = argparse.ArgumentParser() + p.add_argument("--kb-id", required=True, help="Managed KB id (FMKB)") + p.add_argument("--region", default="us-west-2") + p.add_argument("--name-prefix", default="fmkb-sample", + help="prefix for the gateway role / gateway / target") + args = p.parse_args() + + sts = boto3.client("sts").get_caller_identity() + account_id = sts["Account"] + print(f"caller={sts['Arn']} account={account_id} region={args.region}") + + assert_kb_active(args.kb_id, args.region) + print(f"KB {args.kb_id} verified ACTIVE / MANAGED") + + # Deterministic names so a second run reuses the same gateway / target / + # role instead of creating siblings and orphaning the originals. + role_name = f"{args.name_prefix}-gateway-role" + gw_name = f"{args.name_prefix}-gateway" + target_name = f"{args.name_prefix}-target" + + role_arn = get_or_create_gateway_role( + role_name, account_id, args.region, kb_id=args.kb_id, + ) + gw_obj = get_or_create_gateway(gw_name, role_arn, args.region) + target = get_or_create_target(gw_obj["gatewayId"], target_name, args.kb_id, args.region) + + env_path = ROOT / ".env.fmkb-gateway" + env_path.write_text( + "# auto-generated by 01-raw-mcp/setup_gateway.py\n" + f"export REGION={args.region}\n" + f"export ACCOUNT_ID={account_id}\n" + f"export KB_ID={args.kb_id}\n" + f"export GATEWAY_ID={gw_obj['gatewayId']}\n" + f"export GATEWAY_URL={gw_obj['gatewayUrl']}\n" + f"export TARGET_ID={target['targetId']}\n" + f"export TARGET_NAME={target['name']}\n" + f"export GATEWAY_ROLE_NAME={role_name}\n" + ) + print(f"\nwrote {env_path}") + print("source it: source .env.fmkb-gateway") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/rag/managed-knowledge-bases/03-use-case-example/02-fmkb-with-agentcore-runtime/02-strands-agent/README.md b/rag/managed-knowledge-bases/03-use-case-example/02-fmkb-with-agentcore-runtime/02-strands-agent/README.md new file mode 100644 index 000000000..774842386 --- /dev/null +++ b/rag/managed-knowledge-bases/03-use-case-example/02-fmkb-with-agentcore-runtime/02-strands-agent/README.md @@ -0,0 +1,143 @@ +# 02-strands-agent — Strands agent on AgentCore Runtime + +Same shape as the upstream `bedrock-samples / managed-knowledge-bases / 03-use-case-example` notebook, but the agent runs on **AgentCore Runtime** instead of locally. Uses the `agentcore` CLI from `bedrock-agentcore-starter-toolkit`. + +## What's here + +``` +02-strands-agent/ +├── README.md +├── fmkb_gateway_strands.py # the agent — BedrockAgentCoreApp + @app.entrypoint +├── requirements.txt # in-folder copy required by `agentcore configure` +└── iam/ + ├── runtime-trust-policy.json + └── runtime-execution-policy.json # uses bedrock-agentcore:InvokeGateway +``` + +## Prereqs + +1. You've run `01-raw-mcp/setup_gateway.py` and `source ../.env.fmkb-gateway` — that gives you `GATEWAY_URL`, `GATEWAY_ID`, `REGION`, `ACCOUNT_ID`. +2. `pip install bedrock-agentcore-starter-toolkit` for the `agentcore` CLI. + +## Provision the runtime execution role + +```bash +# from 02-strands-agent/ +export AGENT_NAME=fmkb_gateway_agent +ROLE_NAME=AmazonBedrockAgentCoreRuntimeRole-${AGENT_NAME} + +# trust policy +aws iam create-role \ + --role-name "$ROLE_NAME" \ + --assume-role-policy-document file://<(envsubst < iam/runtime-trust-policy.json) + +# permission policy — note the bedrock-agentcore:InvokeGateway statement scoped +# to the gateway ARN, plus the standard Runtime baseline (X-Ray, log delivery, +# CloudWatch metrics under namespace "bedrock-agentcore", InvokeModel*, etc.) +aws iam put-role-policy \ + --role-name "$ROLE_NAME" \ + --policy-name AgentCoreRuntimeInline \ + --policy-document file://<(envsubst < iam/runtime-execution-policy.json) + +export EXECUTION_ROLE_ARN=$(aws iam get-role --role-name "$ROLE_NAME" \ + --query 'Role.Arn' --output text) +``` + +The IAM policies use `${REGION}`, `${ACCOUNT_ID}`, `${AGENT_NAME}`, `${GATEWAY_ID}` placeholders that `envsubst` fills in from the env you sourced. + +## Configure + deploy + +```bash +agentcore configure \ + --name "$AGENT_NAME" \ + --entrypoint fmkb_gateway_strands.py \ + --execution-role "$EXECUTION_ROLE_ARN" \ + --requirements-file requirements.txt \ + --region "$REGION" \ + --non-interactive + +agentcore deploy --agent "$AGENT_NAME" --auto-update-on-conflict \ + -env "GATEWAY_URL=$GATEWAY_URL" \ + -env "AWS_REGION=$REGION" \ + -env "MODEL_ID=us.anthropic.claude-sonnet-4-5-20250929-v1:0" +``` + +## Invoke + +```bash +agentcore invoke --agent "$AGENT_NAME" \ + '{"prompt":"What does the knowledge base say about cat food?"}' +``` + +For a programmatic caller (e.g. a Lambda or another agent), use the data-plane API: + +```python +import boto3, json +client = boto3.client("bedrock-agentcore", region_name="us-west-2") +resp = client.invoke_agent_runtime( + agentRuntimeArn="arn:aws:bedrock-agentcore:us-west-2::runtime/", + qualifier="DEFAULT", + runtimeSessionId="some-id-with-at-least-33-chars-...", + contentType="application/json", + accept="text/event-stream", + payload=json.dumps({"prompt": "..."}).encode(), +) +for chunk in resp["response"].iter_chunks(): + print(chunk.decode(), end="") +``` + +## Local iteration + +```bash +agentcore dev --agent "$AGENT_NAME" # hot-reloading dev server on :8080 +agentcore invoke --agent "$AGENT_NAME" --dev '{"prompt":"…"}' + +# or run as a local container: +agentcore deploy --local --agent "$AGENT_NAME" -env "GATEWAY_URL=$GATEWAY_URL" … +agentcore invoke --agent "$AGENT_NAME" --local '{"prompt":"…"}' +``` + +## Cleanup + +```bash +agentcore destroy --agent "$AGENT_NAME" --force --delete-ecr-repo +aws iam delete-role-policy --role-name "$ROLE_NAME" --policy-name AgentCoreRuntimeInline +aws iam delete-role --role-name "$ROLE_NAME" +``` + +(Then `python ../01-raw-mcp/cleanup.py` to remove the gateway side.) + +## What the agent does + +`fmkb_gateway_strands.py` is the Runtime entrypoint. It: + +- Builds an MCP client over the gateway URL with `mcp_proxy_for_aws.aws_iam_streamablehttp_client` — every MCP request is SigV4-signed using the runtime execution role. +- Calls `mcp_client.list_tools_sync()` to discover the gateway's KB Retrieve tool. +- Hands those tools to a Strands `Agent`. The agent decides when to call Retrieve. +- Yields **text deltas** (`event["data"]`) back through Runtime's SSE channel — the runtime auto-wraps an async-generator `@app.entrypoint` into `text/event-stream`. + +> **Scope note.** The `bedrock-knowledge-bases` connector can also expose +> `AgenticRetrieveStream` as a second tool on the same target. This sample +> wires only `Retrieve` to keep the IAM surface minimal. To enable agentic +> retrieval through the gateway: +> +> 1. Add a second entry to `targetConfiguration.mcp.connector.configurations` +> with `"name": "AgenticRetrieveStream"`. Its `parameterValues` shape is +> different from `Retrieve` — it takes `retrievers` (a list of KB +> references) and a required `agenticRetrieveConfiguration` (foundation +> model + iteration cap), not a single `knowledgeBaseId`. See the AWS +> Gateway Quickstart for the full payload. +> 2. Grant **`bedrock:AgenticRetrieveStream` on `*`** to the **gateway role** +> (not the runtime role). Per the IAM service-authorization reference, +> this action is not scoped to a knowledge-base resource — granting it on +> a KB ARN will fail with AccessDenied at tool-call time. + +## Tested + +This sample was last validated against `bedrock-agentcore` 1.15.0, `strands-agents` 1.44.0, `mcp-proxy-for-aws` 1.6.2, and `mcp` 1.28.0. Newer versions should work; if something breaks, re-pin in `requirements.txt`. To re-validate the inline IAM policy after editing: + +```bash +envsubst < iam/runtime-execution-policy.json > /tmp/perm.json +aws accessanalyzer validate-policy --policy-type IDENTITY_POLICY \ + --policy-document file:///tmp/perm.json +``` diff --git a/rag/managed-knowledge-bases/03-use-case-example/02-fmkb-with-agentcore-runtime/02-strands-agent/fmkb_gateway_strands.py b/rag/managed-knowledge-bases/03-use-case-example/02-fmkb-with-agentcore-runtime/02-strands-agent/fmkb_gateway_strands.py new file mode 100644 index 000000000..99a1d20e3 --- /dev/null +++ b/rag/managed-knowledge-bases/03-use-case-example/02-fmkb-with-agentcore-runtime/02-strands-agent/fmkb_gateway_strands.py @@ -0,0 +1,71 @@ +"""Strands agent on Bedrock AgentCore Runtime, querying a Managed KB through Gateway. + +Runtime invokes `handler` per request. The handler instantiates an MCP client +that talks to the AgentCore Gateway (SigV4-signed when authorizerType=AWS_IAM), +discovers the KB Retrieve tool, and hands control to a Strands agent that +streams text deltas back through Runtime's SSE channel. +""" +from __future__ import annotations + +import logging +import os + +from bedrock_agentcore import BedrockAgentCoreApp +from mcp_proxy_for_aws.client import aws_iam_streamablehttp_client +from strands import Agent +from strands.tools.mcp import MCPClient + +logger = logging.getLogger(__name__) +logging.basicConfig(level=os.environ.get("LOG_LEVEL", "INFO")) + +GATEWAY_URL = os.environ["GATEWAY_URL"] +REGION = os.environ.get("AWS_REGION", "us-west-2") +MODEL_ID = os.environ.get( + "MODEL_ID", "us.anthropic.claude-sonnet-4-5-20250929-v1:0" +) +SYSTEM_PROMPT = os.environ.get( + "SYSTEM_PROMPT", + "You are a knowledge-base assistant. Answer ONLY using information returned " + "by the knowledge base tool. For every user question, call the tool first. " + "If the tool returns no relevant results, reply: \"I don't have that in the " + "knowledge base.\" Do not answer from prior model knowledge. Always cite the " + "source URI(s) from the tool's results when you do answer.", +) + +app = BedrockAgentCoreApp() + + +def _build_mcp_client() -> MCPClient: + return MCPClient( + lambda: aws_iam_streamablehttp_client( + endpoint=GATEWAY_URL, + aws_service="bedrock-agentcore", + aws_region=REGION, + ) + ) + + +@app.entrypoint +async def handler(payload): + prompt = (payload or {}).get("prompt") + if not prompt: + yield {"error": "request must contain a 'prompt' field"} + return + + with _build_mcp_client() as mcp_client: + tools = list(mcp_client.list_tools_sync()) + logger.info("discovered %d MCP tool(s) from gateway", len(tools)) + + agent = Agent( + model=MODEL_ID, + system_prompt=SYSTEM_PROMPT, + tools=tools, + ) + + async for event in agent.stream_async(prompt): + if "data" in event: + yield event["data"] + + +if __name__ == "__main__": + app.run() diff --git a/rag/managed-knowledge-bases/03-use-case-example/02-fmkb-with-agentcore-runtime/02-strands-agent/iam/runtime-execution-policy.json b/rag/managed-knowledge-bases/03-use-case-example/02-fmkb-with-agentcore-runtime/02-strands-agent/iam/runtime-execution-policy.json new file mode 100644 index 000000000..c5629f37c --- /dev/null +++ b/rag/managed-knowledge-bases/03-use-case-example/02-fmkb-with-agentcore-runtime/02-strands-agent/iam/runtime-execution-policy.json @@ -0,0 +1,92 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "ECRImageAccess", + "Effect": "Allow", + "Action": [ + "ecr:BatchGetImage", + "ecr:GetDownloadUrlForLayer" + ], + "Resource": "arn:aws:ecr:${REGION}:${ACCOUNT_ID}:repository/*" + }, + { + "Sid": "ECRTokenAccess", + "Effect": "Allow", + "Action": "ecr:GetAuthorizationToken", + "Resource": "*" + }, + { + "Sid": "RuntimeLogGroup", + "Effect": "Allow", + "Action": [ + "logs:CreateLogGroup", + "logs:DescribeLogGroups" + ], + "Resource": "arn:aws:logs:${REGION}:${ACCOUNT_ID}:log-group:/aws/bedrock-agentcore/runtimes/*" + }, + { + "Sid": "RuntimeLogStream", + "Effect": "Allow", + "Action": [ + "logs:CreateLogStream", + "logs:DescribeLogStreams", + "logs:PutLogEvents" + ], + "Resource": "arn:aws:logs:${REGION}:${ACCOUNT_ID}:log-group:/aws/bedrock-agentcore/runtimes/*:log-stream:*" + }, + { + "Sid": "XRay", + "Effect": "Allow", + "Action": [ + "xray:PutTraceSegments", + "xray:PutTelemetryRecords", + "xray:GetSamplingRules", + "xray:GetSamplingTargets" + ], + "Resource": "*" + }, + { + "Sid": "AgentCoreCloudWatchMetrics", + "Effect": "Allow", + "Action": "cloudwatch:PutMetricData", + "Resource": "*", + "Condition": { + "StringEquals": { + "cloudwatch:namespace": "bedrock-agentcore" + } + } + }, + { + "Sid": "GetAgentAccessToken", + "Effect": "Allow", + "Action": [ + "bedrock-agentcore:GetWorkloadAccessToken", + "bedrock-agentcore:GetWorkloadAccessTokenForJWT", + "bedrock-agentcore:GetWorkloadAccessTokenForUserId" + ], + "Resource": [ + "arn:aws:bedrock-agentcore:${REGION}:${ACCOUNT_ID}:workload-identity-directory/default", + "arn:aws:bedrock-agentcore:${REGION}:${ACCOUNT_ID}:workload-identity-directory/default/workload-identity/${AGENT_NAME}-*" + ] + }, + { + "Sid": "BedrockModelInvocation", + "Effect": "Allow", + "Action": [ + "bedrock:InvokeModel", + "bedrock:InvokeModelWithResponseStream" + ], + "Resource": [ + "arn:aws:bedrock:*::foundation-model/*", + "arn:aws:bedrock:${REGION}:${ACCOUNT_ID}:inference-profile/*" + ] + }, + { + "Sid": "InvokeAgentCoreGateway", + "Effect": "Allow", + "Action": "bedrock-agentcore:InvokeGateway", + "Resource": "arn:aws:bedrock-agentcore:${REGION}:${ACCOUNT_ID}:gateway/${GATEWAY_ID}" + } + ] +} diff --git a/rag/managed-knowledge-bases/03-use-case-example/02-fmkb-with-agentcore-runtime/02-strands-agent/iam/runtime-trust-policy.json b/rag/managed-knowledge-bases/03-use-case-example/02-fmkb-with-agentcore-runtime/02-strands-agent/iam/runtime-trust-policy.json new file mode 100644 index 000000000..ff95ec97d --- /dev/null +++ b/rag/managed-knowledge-bases/03-use-case-example/02-fmkb-with-agentcore-runtime/02-strands-agent/iam/runtime-trust-policy.json @@ -0,0 +1,21 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "AssumeRolePolicy", + "Effect": "Allow", + "Principal": { + "Service": "bedrock-agentcore.amazonaws.com" + }, + "Action": "sts:AssumeRole", + "Condition": { + "StringEquals": { + "aws:SourceAccount": "${ACCOUNT_ID}" + }, + "ArnLike": { + "aws:SourceArn": "arn:aws:bedrock-agentcore:${REGION}:${ACCOUNT_ID}:*" + } + } + } + ] +} diff --git a/rag/managed-knowledge-bases/03-use-case-example/02-fmkb-with-agentcore-runtime/02-strands-agent/requirements.txt b/rag/managed-knowledge-bases/03-use-case-example/02-fmkb-with-agentcore-runtime/02-strands-agent/requirements.txt new file mode 100644 index 000000000..d5c62f673 --- /dev/null +++ b/rag/managed-knowledge-bases/03-use-case-example/02-fmkb-with-agentcore-runtime/02-strands-agent/requirements.txt @@ -0,0 +1,10 @@ +bedrock-agentcore>=1.15.0 +strands-agents>=1.44.0 +mcp>=1.28.0 +mcp-proxy-for-aws>=1.6.2 +# botocore 1.43.32 is the first release that models the bedrock-knowledge-bases +# connector under bedrock-agentcore-control.CreateGatewayTarget. Earlier +# releases reject the call client-side with ParamValidationError on the +# unknown `connector` key. +boto3>=1.43.32 +botocore>=1.43.32 diff --git a/rag/managed-knowledge-bases/03-use-case-example/02-fmkb-with-agentcore-runtime/README.md b/rag/managed-knowledge-bases/03-use-case-example/02-fmkb-with-agentcore-runtime/README.md new file mode 100644 index 000000000..f0aac1d6f --- /dev/null +++ b/rag/managed-knowledge-bases/03-use-case-example/02-fmkb-with-agentcore-runtime/README.md @@ -0,0 +1,104 @@ +# Connect your agent to a Bedrock Managed Knowledge Base via AgentCore Gateway + +This sample shows how to expose a Bedrock **Managed Knowledge Base (FMKB)** as an MCP tool through **AgentCore Gateway**, then have an agent — running on **AgentCore Runtime** — query that tool. Follows the same shape as the other folders under `01-features/03-connect-your-agent-to-anything`. + +``` + ┌─────────────────────┐ InvokeAgentRuntime ┌────────────────────────┐ +caller ─► AgentCore Runtime ───────────────────────────► Strands agent │ + │ (managed compute) │ │ @app.entrypoint │ + └──────────┬───────────┘ └──────────┬─────────────┘ + │ SigV4 (mcp-proxy-for-aws) │ + ▼ ▼ + ┌──────────────────────┐ Retrieve via ┌────────────────────┐ + │ AgentCore Gateway ├─── bedrock-knowledge-bases─► FMKB (managed) │ + │ MCP target (AWS_IAM)│ connector │ │ + └──────────────────────┘ └────────────────────┘ +``` + +## What's in here + +``` +04-fmkb-managed-kb/ +├── README.md +├── requirements.txt # deps for 01-raw-mcp (02-strands-agent has its own copy) +├── utils/ +│ ├── managed_kb.py # helper: create/reuse a managed KB + S3 source +│ └── gateway.py # helper: create gateway + KB target +├── 01-raw-mcp/ # call the gateway directly via MCP+SigV4 (no agent) +│ ├── README.md +│ ├── setup_gateway.py +│ ├── raw_mcp_call.py +│ └── cleanup.py +├── 02-strands-agent/ # Strands agent on AgentCore Runtime, deployed via CLI +│ ├── README.md +│ ├── fmkb_gateway_strands.py # the @app.entrypoint +│ └── iam/ +│ ├── runtime-trust-policy.json +│ └── runtime-execution-policy.json +└── images/ # (architecture diagrams) +``` + +## Prerequisites + +1. **Python 3.10+** (`bedrock-agentcore` and `strands-agents` require it). +2. **AWS account in a region where AgentCore is available** (`us-west-2` recommended). +3. **Bedrock model access** to a Sonnet inference profile, e.g. `us.anthropic.claude-sonnet-4-5-20250929-v1:0`. +4. **An existing Managed Knowledge Base** with at least one ingested document. If you don't have one, create one with [`bedrock-samples / rag / managed-knowledge-bases / 01-getting-started/01-create-bmkb-s3.ipynb`](https://github.com/aws-samples/amazon-bedrock-samples/tree/main/rag/managed-knowledge-bases/01-getting-started). Note its `KB_ID`. +5. **AgentCore CLI** (used by `02-strands-agent`): + ```bash + pip install bedrock-agentcore-starter-toolkit + ``` +6. **Caller principal permissions** as documented in [Use the AgentCore CLI](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/runtime-permissions.html#runtime-permissions-cli). + +## Quick start + +```bash +# from this folder +pip install -r requirements.txt + +# 1. Create the gateway + KB target (writes .env.fmkb-gateway). +# `--name-prefix` controls the gateway-side resource names (gateway role, +# gateway, KB target); the runtime/agent name is set separately in step 3. +python 01-raw-mcp/setup_gateway.py --kb-id --name-prefix fmkb-sample +source .env.fmkb-gateway + +# 2. Verify the gateway path with a raw MCP call (no agent) +python 01-raw-mcp/raw_mcp_call.py "What does the knowledge base say about X?" + +# 3. Deploy the agent to Runtime and invoke it +cd 02-strands-agent +agentcore configure \ + --name fmkb_gateway_agent \ + --entrypoint fmkb_gateway_strands.py \ + --requirements-file requirements.txt \ + --region "$REGION" \ + --non-interactive +agentcore deploy --agent fmkb_gateway_agent --auto-update-on-conflict \ + -env "GATEWAY_URL=$GATEWAY_URL" \ + -env "AWS_REGION=$REGION" \ + -env "MODEL_ID=us.anthropic.claude-sonnet-4-5-20250929-v1:0" +agentcore invoke --agent fmkb_gateway_agent \ + '{"prompt":"What does the knowledge base say about X?"}' + +# 4. Tear down +cd .. +python 01-raw-mcp/cleanup.py +agentcore destroy --agent fmkb_gateway_agent --force --delete-ecr-repo +``` + +The gateway uses `authorizerType=AWS_IAM`. The agent reaches it with SigV4-signed MCP via the `mcp-proxy-for-aws` library; the runtime execution role must allow `bedrock-agentcore:InvokeGateway` on the gateway ARN. + +## Why pick what + +- **`01-raw-mcp`** — confirms the gateway → KB plumbing works without any agent in the loop. Use this first when something breaks. +- **`02-strands-agent`** — production shape: agent deployed to Runtime, callable from anywhere via `bedrock-agentcore:InvokeAgentRuntime`. + +## Tested against + +Last validated with `bedrock-agentcore` 1.15.0, `strands-agents` 1.44.0, `mcp-proxy-for-aws` 1.6.2, `mcp` 1.28.0, and `boto3` / `botocore` 1.43.32. The `botocore` floor is non-trivial — earlier releases don't model `targetConfiguration.mcp.connector` for the bedrock-knowledge-bases connector and will reject `create_gateway_target` client-side. + +If you hit a surprise after upgrading any of these, the most informative bisection point is `botocore` — service-model changes show up there first. + +## Cleanup + +`01-raw-mcp/cleanup.py` removes the gateway + KB target it created. `agentcore destroy` removes the runtime, ECR repo, CodeBuild project, log group, and execution role. The KB itself is *not* touched — you own its lifecycle. diff --git a/rag/managed-knowledge-bases/03-use-case-example/02-fmkb-with-agentcore-runtime/requirements.txt b/rag/managed-knowledge-bases/03-use-case-example/02-fmkb-with-agentcore-runtime/requirements.txt new file mode 100644 index 000000000..d5c62f673 --- /dev/null +++ b/rag/managed-knowledge-bases/03-use-case-example/02-fmkb-with-agentcore-runtime/requirements.txt @@ -0,0 +1,10 @@ +bedrock-agentcore>=1.15.0 +strands-agents>=1.44.0 +mcp>=1.28.0 +mcp-proxy-for-aws>=1.6.2 +# botocore 1.43.32 is the first release that models the bedrock-knowledge-bases +# connector under bedrock-agentcore-control.CreateGatewayTarget. Earlier +# releases reject the call client-side with ParamValidationError on the +# unknown `connector` key. +boto3>=1.43.32 +botocore>=1.43.32 diff --git a/rag/managed-knowledge-bases/03-use-case-example/02-fmkb-with-agentcore-runtime/utils/__init__.py b/rag/managed-knowledge-bases/03-use-case-example/02-fmkb-with-agentcore-runtime/utils/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/rag/managed-knowledge-bases/03-use-case-example/02-fmkb-with-agentcore-runtime/utils/gateway.py b/rag/managed-knowledge-bases/03-use-case-example/02-fmkb-with-agentcore-runtime/utils/gateway.py new file mode 100644 index 000000000..30e155102 --- /dev/null +++ b/rag/managed-knowledge-bases/03-use-case-example/02-fmkb-with-agentcore-runtime/utils/gateway.py @@ -0,0 +1,131 @@ +"""Helpers to create / tear down an AgentCore Gateway with an FMKB target.""" +from __future__ import annotations + +import time +from typing import Optional + +import boto3 + + +def create_gateway(name: str, role_arn: str, region: str) -> dict: + control = boto3.client("bedrock-agentcore-control", region_name=region) + g = control.create_gateway( + name=name, roleArn=role_arn, protocolType="MCP", authorizerType="AWS_IAM", + ) + gw_id = g["gatewayId"] + for _ in range(60): + s = control.get_gateway(gatewayIdentifier=gw_id) + if s["status"] == "READY": + return s + if s["status"] in ("FAILED",): + raise RuntimeError(f"gateway {gw_id} entered {s['status']}") + time.sleep(5) + raise RuntimeError(f"gateway {gw_id} did not reach READY in time") + + +def create_kb_target(gateway_id: str, kb_id: str, name: str, region: str, + num_results: int = 5) -> dict: + """Create the bedrock-knowledge-bases connector target on the gateway. + + Requires botocore >= 1.43.32 — earlier versions don't model + `targetConfiguration.mcp.connector` and will reject the call client-side. + """ + control = boto3.client("bedrock-agentcore-control", region_name=region) + target_config = { + "mcp": { + "connector": { + "source": {"connectorId": "bedrock-knowledge-bases"}, + "configurations": [{ + "name": "Retrieve", + "description": "Search the knowledge base for relevant documents.", + "parameterValues": { + "knowledgeBaseId": kb_id, + "retrievalConfiguration": { + "managedSearchConfiguration": {"numberOfResults": num_results} + }, + }, + "parameterOverrides": [ + {"path": "$.retrievalQuery.text", + "description": "Search query for the knowledge base.", + "visible": True}, + {"path": "$.retrievalConfiguration.managedSearchConfiguration.numberOfResults", + "description": "Number of results to retrieve (1-100).", + "visible": True}, + ], + }], + } + } + } + created = control.create_gateway_target( + gatewayIdentifier=gateway_id, + name=name, + description=f"FMKB Retrieve target for {kb_id}", + credentialProviderConfigurations=[ + {"credentialProviderType": "GATEWAY_IAM_ROLE"} + ], + targetConfiguration=target_config, + ) + target_id = created["targetId"] + for _ in range(60): + s = control.get_gateway_target(gatewayIdentifier=gateway_id, targetId=target_id) + if s["status"] == "READY": + return s + if s["status"] == "FAILED": + reasons = s.get("statusReasons") or [s.get("failureMessage")] + raise RuntimeError(f"target {target_id} FAILED: {reasons}") + time.sleep(5) + raise RuntimeError(f"target {target_id} did not reach READY in time") + + +def delete_target(gateway_id: str, target_id: str, region: str) -> None: + boto3.client("bedrock-agentcore-control", region_name=region) \ + .delete_gateway_target(gatewayIdentifier=gateway_id, targetId=target_id) + + +def delete_gateway(gateway_id: str, region: str) -> None: + boto3.client("bedrock-agentcore-control", region_name=region) \ + .delete_gateway(gatewayIdentifier=gateway_id) + + +def gateway_role_trust_policy(account_id: str, region: str) -> dict: + return { + "Version": "2012-10-17", + "Statement": [{ + "Effect": "Allow", + "Principal": {"Service": "bedrock-agentcore.amazonaws.com"}, + "Action": "sts:AssumeRole", + "Condition": { + "StringEquals": {"aws:SourceAccount": account_id}, + "ArnLike": { + "aws:SourceArn": f"arn:aws:bedrock-agentcore:{region}:{account_id}:gateway/*" + }, + }, + }], + } + + +def gateway_role_permission_policy( + account_id: str, region: str, kb_id: Optional[str] = None, +) -> dict: + """Least-privilege policy for the gateway execution role. + + Only `bedrock:Retrieve` and `bedrock:GetKnowledgeBase` are needed — + `Retrieve` is the runtime call, `GetKnowledgeBase` is the validation call + the gateway makes when you create a KB target. + """ + kb_arn = ( + f"arn:aws:bedrock:{region}:{account_id}:knowledge-base/{kb_id}" + if kb_id else f"arn:aws:bedrock:{region}:{account_id}:knowledge-base/*" + ) + return { + "Version": "2012-10-17", + "Statement": [{ + "Sid": "RetrieveAndDescribe", + "Effect": "Allow", + "Action": [ + "bedrock:Retrieve", + "bedrock:GetKnowledgeBase", + ], + "Resource": kb_arn, + }], + } diff --git a/rag/managed-knowledge-bases/03-use-case-example/02-fmkb-with-agentcore-runtime/utils/managed_kb.py b/rag/managed-knowledge-bases/03-use-case-example/02-fmkb-with-agentcore-runtime/utils/managed_kb.py new file mode 100644 index 000000000..31323773b --- /dev/null +++ b/rag/managed-knowledge-bases/03-use-case-example/02-fmkb-with-agentcore-runtime/utils/managed_kb.py @@ -0,0 +1,29 @@ +"""Optional helper to verify a Managed KB is active before wiring up the gateway. + +This file does NOT create a KB — that belongs to bedrock-samples / +managed-knowledge-bases / 01-getting-started. It just describes/validates +what the user passes in. +""" +from __future__ import annotations + +import boto3 +from botocore.exceptions import ClientError + + +def assert_kb_active(kb_id: str, region: str) -> dict: + agent = boto3.client("bedrock-agent", region_name=region) + try: + kb = agent.get_knowledge_base(knowledgeBaseId=kb_id)["knowledgeBase"] + except ClientError as e: + raise SystemExit( + f"KB {kb_id} not found in {region}: {e}. Create one with " + f"bedrock-samples / managed-knowledge-bases / 01-getting-started first." + ) + if kb["status"] != "ACTIVE": + raise SystemExit(f"KB {kb_id} is in status {kb['status']}, expected ACTIVE.") + if kb["knowledgeBaseConfiguration"]["type"] != "MANAGED": + raise SystemExit( + f"KB {kb_id} is type {kb['knowledgeBaseConfiguration']['type']}; " + "this sample requires type=MANAGED." + ) + return kb