Home / Articles

Enterprise MCP on Bedrock AgentCore with Okta SSO and Trino

Lambda OAuth brain, PKCE relay, AgentCore gateway and runtime, and per-user Trino connections without leaking shared tokens.

Publisher: REACTAPP.TOP13 min read2982 words

This article is published in English.

Sources: Building Enterprise MCP on AWS Bedrock AgentCore: Okta SSO + Trino

This walkthrough rebuilds the path from raw materials to a working system for: Building Enterprise MCP on AWS Bedrock AgentCore: Okta SSO + Trino. The focus is operable steps, explicit checks, and code that you can drop into a repo without guessing intent. For Overview, define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state. Record timings and token or query cost next to functional results. Cost visibility early prevents surprise bills when the path moves from demo to shared environments.

TL;DR

For TL;DR, define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state. Keep configuration outside application code. Environment files, secret stores, and feature flags belong in one place operators can audit without reading the whole graph. Expose tools with narrow schemas and explicit side-effect labels. Hosts need to know which calls mutate state before they auto-approve.

MCP Client → Lambda OAuth Proxy → Bedrock AgentCore Gateway → AgentCore Runtime → Trino

Why handing a raw token to a tool fails enterprise checks

For Why handing a raw token to a tool fails enterprise checks, define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state. Document the happy path and the recovery path together. Retries, human gates, and dead-letter handling are part of the product, not later polish. Expose tools with narrow schemas and explicit side-effect labels. Hosts need to know which calls mutate state before they auto-approve.

Architecture at a glance

For Architecture at a glance, define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state. Prefer small, testable units over sprawling scripts. When a step fails, the failure should point at a single responsibility rather than a tangled pipeline. Expose tools with narrow schemas and explicit side-effect labels. Hosts need to know which calls mutate state before they auto-approve. For Architecture at a glance, define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state. Record timings and token or query cost next to functional results. Cost visibility early prevents surprise bills when the path moves from demo to shared environments.

1. The MCP client

For 1. The MCP client, define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state. Keep configuration outside application code. Environment files, secret stores, and feature flags belong in one place operators can audit without reading the whole graph. Authenticate at the gateway and re-authorize at the data plane. A bearer token alone is not a tenancy boundary.

2. The Lambda MCP proxy (the OAuth brain)

For 2. The Lambda MCP proxy (the OAuth brain), define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state. Document the happy path and the recovery path together. Retries, human gates, and dead-letter handling are part of the product, not later polish. Authenticate at the gateway and re-authorize at the data plane. A bearer token alone is not a tenancy boundary.

3. The AWS Bedrock AgentCore Gateway

For 3. The AWS Bedrock AgentCore Gateway, define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state. Prefer small, testable units over sprawling scripts. When a step fails, the failure should point at a single responsibility rather than a tangled pipeline. Authenticate at the gateway and re-authorize at the data plane. A bearer token alone is not a tenancy boundary. For 3. The AWS Bedrock AgentCore Gateway, define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state. Record timings and token or query cost next to functional results. Cost visibility early prevents surprise bills when the path moves from demo to shared environments.

4. The AWS Bedrock AgentCore Runtime

For 4. The AWS Bedrock AgentCore Runtime, define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state. Keep configuration outside application code. Environment files, secret stores, and feature flags belong in one place operators can audit without reading the whole graph. Log tool name, args hash, latency, and outcome for every call. Debugging agent loops without that trail wastes hours.

The end-to-end auth flow

For The end-to-end auth flow, define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state. Document the happy path and the recovery path together. Retries, human gates, and dead-letter handling are part of the product, not later polish. Log tool name, args hash, latency, and outcome for every call. Debugging agent loops without that trail wastes hours.

┌─────────────────────┐
│    MCP Client       │
│ Claude / Claude Code│
└──────────┬──────────┘
           │
           │ OAuth 2.1 + PKCE
           ▼
┌─────────────────────┐
│   Lambda MCP Proxy  │◄──────────────► Okta
│ OAuth / Identity    │   Login + JWT
└──────────┬──────────┘
           │
           │ Bearer JWT
           ▼
┌─────────────────────────────┐
│ Bedrock AgentCore Gateway   │
│ CUSTOM_JWT validation       │
└────────────┬────────────────┘
             │ SigV4
             ▼
┌─────────────────────────────┐
│ Bedrock AgentCore Runtime   │
│ FastMCP Server              │
│                             │
│ list_catalogs               │
│ describe_table              │
│ execute_query               │
└────────────┬────────────────┘
             │
             │ User JWT
             ▼
┌─────────────────────────────┐
│ Enterprise Query Engine     │
│ Per-user authorization      │
└─────────────────────────────┘

Deep dive 1: Teaching the Lambda proxy to speak OAuth 2.1

For Deep dive 1: Teaching the Lambda proxy to speak OAuth 2.1, define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state. Prefer small, testable units over sprawling scripts. When a step fails, the failure should point at a single responsibility rather than a tangled pipeline. Log tool name, args hash, latency, and outcome for every call. Debugging agent loops without that trail wastes hours. For Deep dive 1: Teaching the Lambda proxy to speak OAuth 2.1, define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state. Record timings and token or query cost next to functional results. Cost visibility early prevents surprise bills when the path moves from demo to shared environments.

# GET /.well-known/protected-resource
{
  "resource": f"{BASE_URL}/mcp",
  "authorization_servers": [BASE_URL],
}
# GET /.well-known/oauth-authorization-server
{
  "issuer": BASE_URL,
  "authorization_endpoint": f"{BASE_URL}/authorize",
  "token_endpoint":         f"{BASE_URL}/token",
  "registration_endpoint":  f"{BASE_URL}/register",
  "response_types_supported": ["code"],
  "grant_types_supported": ["authorization_code", "refresh_token"],
  "code_challenge_methods_supported": ["S256"],
  "token_endpoint_auth_methods_supported": ["none"],
}

Relaying PKCE without breaking it

For Relaying PKCE without breaking it, define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state. Keep configuration outside application code. Environment files, secret stores, and feature flags belong in one place operators can audit without reading the whole graph. Expose tools with narrow schemas and explicit side-effect labels. Hosts need to know which calls mutate state before they auto-approve.

relay_payload = json.dumps({"u": original_redirect, "s": original_state}).encode()
relay_state   = base64.urlsafe_b64encode(relay_payload).decode().rstrip("=")
params = {
    "client_id":             OKTA_CLIENT_ID,
    "response_type":         "code",
    "redirect_uri":          f"{BASE_URL}/callback",   # <- proxy's callback
    "state":                 relay_state,               # <- client's state, wrapped
    "code_challenge":        qs["code_challenge"],      # <- untouched
    "code_challenge_method": "S256",
}

Dynamic client registration

For Dynamic client registration, define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state. Document the happy path and the recovery path together. Retries, human gates, and dead-letter handling are part of the product, not later polish. Expose tools with narrow schemas and explicit side-effect labels. Hosts need to know which calls mutate state before they auto-approve.

def handle_register(event):
    req = json.loads(event.get("body") or "{}")
    return _json_resp(201, {
        "client_id": OKTA_CLIENT_ID,
        "redirect_uris": req.get("redirect_uris", []),
        "grant_types":   req.get("grant_types", ["authorization_code"]),
        "response_types": req.get("response_types", ["code"]),
        "token_endpoint_auth_method": "none",
    })

Deep dive 2: Breakage observed when FastMCP sat behind AgentCore

For Deep dive 2: Breakage observed when FastMCP sat behind AgentCore, define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state. Prefer small, testable units over sprawling scripts. When a step fails, the failure should point at a single responsibility rather than a tangled pipeline. Expose tools with narrow schemas and explicit side-effect labels. Hosts need to know which calls mutate state before they auto-approve. For Deep dive 2: Breakage observed when FastMCP sat behind AgentCore, define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state. Record timings and token or query cost next to functional results. Cost visibility early prevents surprise bills when the path moves from demo to shared environments.

Sharp edge #1: No custom HTTP headers reach the container

For Sharp edge #1: No custom HTTP headers reach the container, define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state. Keep configuration outside application code. Environment files, secret stores, and feature flags belong in one place operators can audit without reading the whole graph. Authenticate at the gateway and re-authorize at the data plane. A bearer token alone is not a tenancy boundary.

meta = payload.setdefault("params", {}).setdefault("_meta", {})
meta["okta_token"]   = token         # the validated user identity
meta["trino_target"] = routing_hint  # optional routing metadata
class CredentialExtractMiddleware:
    async def __call__(self, scope, receive, send):
        # buffer the whole body
        body = await _read_full_body(receive)
        # pull identity out of params._meta
        meta = json.loads(body).get("params", {}).get("_meta", {})
        token = meta.get("okta_token")        # AgentCore also injects the validated caller token as a header —
        # prefer that when present, fall back to _meta / Authorization otherwise
        token = token or _caller_token_from_headers(scope)        _req_okta_token.set(token)        # replay the buffered body so the MCP app can read it too
        await self.app(scope, _replay(body, receive), send)

Sharp edge #2: AgentCore strips the Accept header

For Sharp edge #2: AgentCore strips the Accept header, define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state. Document the happy path and the recovery path together. Retries, human gates, and dead-letter handling are part of the product, not later polish. Authenticate at the gateway and re-authorize at the data plane. A bearer token alone is not a tenancy boundary.

class InjectAcceptMiddleware:
    async def __call__(self, scope, receive, send):
        if scope["type"] == "http":
            headers = [(k, v) for k, v in scope["headers"] if k != b"accept"]
            headers.append((b"accept", b"application/json, text/event-stream"))
            scope = {**scope, "headers": headers}
        await self.app(scope, receive, send)

Deep dive 3: Mapping Okta access tokens onto isolated Trino sessions

For Deep dive 3: Mapping Okta access tokens onto isolated Trino sessions, define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state. Prefer small, testable units over sprawling scripts. When a step fails, the failure should point at a single responsibility rather than a tangled pipeline. Authenticate at the gateway and re-authorize at the data plane. A bearer token alone is not a tenancy boundary. For Deep dive 3: Mapping Okta access tokens onto isolated Trino sessions, define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state. Record timings and token or query cost next to functional results. Cost visibility early prevents surprise bills when the path moves from demo to shared environments.

def get_connection():
    token = _req_okta_token.get()
    user  = _jwt_email(token)          # e.g. "alice@company.com"
    return backend.connect(
        host=BACKEND_HOST,
        user=user,                      # identity for attribution/authorization
        auth=JWTAuthentication(token),  # the backend re-validates this itself
        http_scheme="https",
    )

Authorization and read-only enforcement ultimately belong at the data layer.

For Authorization and read-only enforcement ultimately belong at the data layer., define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state. Keep configuration outside application code. Environment files, secret stores, and feature flags belong in one place operators can audit without reading the whole graph. Log tool name, args hash, latency, and outcome for every call. Debugging agent loops without that trail wastes hours.

forbidden = ("INSERT", "UPDATE", "DELETE", "DROP", "CREATE",
             "ALTER", "TRUNCATE", "MERGE", "REPLACE", "GRANT", "REVOKE")
if any(query.strip().upper().startswith(kw) for kw in forbidden):
    raise ValueError("Write operations are not allowed.")

Two ways to connect the client

For Two ways to connect the client, define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state. Document the happy path and the recovery path together. Retries, human gates, and dead-letter handling are part of the product, not later polish. Log tool name, args hash, latency, and outcome for every call. Debugging agent loops without that trail wastes hours.

{ "mcpServers": { "data": { "type": "http", "url": "https://<lambda-url>/mcp" } } }
{ "mcpServers": { "data": { "type": "http", "url": "http://localhost:8766/mcp" } } }

What we learned

For What we learned, define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state. Prefer small, testable units over sprawling scripts. When a step fails, the failure should point at a single responsibility rather than a tangled pipeline. Log tool name, args hash, latency, and outcome for every call. Debugging agent loops without that trail wastes hours. For What we learned, define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state. Record timings and token or query cost next to functional results. Cost visibility early prevents surprise bills when the path moves from demo to shared environments.

Wrapping up

For Wrapping up, define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state. Keep configuration outside application code. Environment files, secret stores, and feature flags belong in one place operators can audit without reading the whole graph. Expose tools with narrow schemas and explicit side-effect labels. Hosts need to know which calls mutate state before they auto-approve.

Operational checklist

For Operational checklist, define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state.

Treat this stage as a contract between inputs and validated outputs. Name the artifacts, define success checks, and refuse silent partial completion.

Authenticate at the gateway and re-authorize at the data plane. A bearer token alone is not a tenancy boundary.

Checkpoint after expensive steps. Resume should not re-bill the same LLM call when an operator retries a later node.

Carry identity from the client to the downstream store without collapsing everyone into a service account.

Add a smoke test that exercises the critical path in CI with fixtures, not live paid APIs, whenever budgets allow.

Before promoting the stack, freeze versions, capture a golden transcript for the critical path, and confirm rollback steps. Shared environments need rate limits, tenancy checks, and a clear owner for secret rotation. Prefer boring reliability over clever one-off demos.

Deployment note 1: pin images, set request budgets, and verify tenant isolation on a canary before wider rollouts.

Deployment note 2: pin images, set request budgets, and verify tenant isolation on a canary before wider rollouts.

Deployment note 3: pin images, set request budgets, and verify tenant isolation on a canary before wider rollouts.

Deployment note 4: pin images, set request budgets, and verify tenant isolation on a canary before wider rollouts.

Deployment note 5: pin images, set request budgets, and verify tenant isolation on a canary before wider rollouts.

Deployment note 6: pin images, set request budgets, and verify tenant isolation on a canary before wider rollouts.