Skip to content
Latest (currently 2026.7.0) has the newest features, bug fixes, and CVE patches of Solo Enterprise for agentregistry.

AWS Bedrock AgentCore

Page as Markdown

Set up Solo Enterprise for agentregistry on an EKS cluster and deploy agents and MCP servers to AWS Bedrock AgentCore with VPC networking through a managed agentgateway.

Solo Enterprise for agentregistry is an enterprise-grade AI artifact platform to manage the lifecycle of your AI artifacts, including AI agents, MCP tool servers, skills, and more. With Solo Enterprise for agentregistry, you can easily orchestrate deployments across runtimes, and monitor the efficiency and cost of your AI deployments. The product builds on the open-source agentregistry project. To learn more about Solo Enterprise for agentregistry, see About.

About this guide

This guide walks you through the full setup of Solo Enterprise for agentregistry, from authentication to deploying your first AI agent and MCP server to AWS Bedrock AgentCore with VPC networking. By the end, you have:

  • A Keycloak identity provider that runs in your cluster and is configured with the realm, clients, and users that Solo Enterprise for agentregistry requires for OIDC authentication.
  • An Solo Enterprise for agentregistry control plane that is secured with OIDC authentication and backed by PostgreSQL and ClickHouse.
  • The arctl CLI that can authenticate with Solo Enterprise for agentregistry so that you can get started with managing and deploying AI artifacts.
  • The Solo Enterprise for agentregistry UI that you can use to browse and manage AI artifacts in the registry catalog.
  • A connection from the registry to AWS Bedrock AgentCore, using either IAM user credentials or EKS Pod Identity.
  • A managed gateway in your VPC that routes all agent and MCP server traffic and enforces access policies.
  • A Python ADK agent that uses the AWS Bedrock Claude provider. You build and publish the agent to the registry catalog. Then, you use Solo Enterprise for agentregistry to deploy the agent to AWS Bedrock AgentCore inside your VPC.
  • A source-built MCP server registered in the registry catalog and deployed to AWS Bedrock AgentCore inside the same VPC, so the agent can reach it over the private network.

The following diagram shows the components that you set up and how they relate to each other.

Before you begin

  1. Install the following CLIs.

    • helm: Install the Solo Enterprise for agentregistry Helm chart.
    • kubectl: Interact with your Kubernetes cluster.
    • aws: Interact with AWS Bedrock AgentCore and your EKS cluster.
    • eksctl: Create the IAM service account for the AWS Load Balancer Controller.
    • jq: Used as a script wrapper in this guide.
    • openssl: Generate a unique private key for JWT signing. Install with brew install openssl.
  2. Create or use an existing AWS VPC. You deploy your AWS Bedrock AgentCore runtimes, managed gateway, and EKS cluster into this VPC to allow secure private networking between agents and MCP servers. Make sure that your VPC has the following configuration:

    • At least one private subnet.
    • A security group that allows inbound traffic from within the VPC. The default security group in the VPC is sufficient for this guide.
  3. Save the networking details of your VPC as environment variables.

    1. Set the VPC ID and AWS region.

      export VPC_ID=<vpc-ID>
      export AWS_REGION=<aws-region>
    2. Retrieve the private subnet IDs from the VPC. Private subnets are identified by map-public-ip-on-launch=false. The gateway that you later deploy in this guide uses a single subnet. The agent and MCP server runtime deployments however use all available private subnets for redundancy.

      # Single private subnet for the gateway
      export SUBNET_ID=$(aws ec2 describe-subnets \
        --region "${AWS_REGION}" \
        --filters "Name=vpc-id,Values=${VPC_ID}" "Name=map-public-ip-on-launch,Values=false" \
        --query 'Subnets[0].SubnetId' \
        --output text)
      
      # All private subnets formatted as YAML for Deployment specs
      export SUBNET_IDS_YAML=$(aws ec2 describe-subnets \
        --region "${AWS_REGION}" \
        --filters "Name=vpc-id,Values=${VPC_ID}" "Name=map-public-ip-on-launch,Values=false" \
        --query 'Subnets[].SubnetId' \
        --output json | jq -r '.[] | "      - " + .')
      
      echo "Gateway subnet: ${SUBNET_ID}"
      echo "Agent and MCP server deployment subnets:"
      echo "${SUBNET_IDS_YAML}"
    3. Retrieve the default security group ID for the VPC.

      export SG_ID=$(aws ec2 describe-security-groups \
        --region "${AWS_REGION}" \
        --filters "Name=vpc-id,Values=${VPC_ID}" "Name=group-name,Values=default" \
        --query 'SecurityGroups[0].GroupId' \
        --output text)
      echo "SG_ID: ${SG_ID}"
  4. Create or use an existing Amazon EKS cluster and save the name in an environment variable. Make sure that the cluster is deployed into the VPC that you created earlier. You use the EKS cluster to install Solo Enterprise for agentregistry. For more information about how to create the cluster, see the EKS documentation.

    export EKS_CLUSTER_NAME=<your-eks-cluster-name>
  5. Install the AWS Load Balancer Controller in your EKS cluster. Keycloak and the OpenTelemetry collector are both exposed as LoadBalancer services.

    1. Associate an OIDC provider with your cluster. This is required to use IAM Roles for Service Accounts (IRSA), which the controller uses to call AWS APIs.

      eksctl utils associate-iam-oidc-provider \
        --region $AWS_REGION \
        --cluster $EKS_CLUSTER_NAME \
        --approve
    2. Download the IAM policy that grants the controller the permissions it needs.

      curl -O https://raw.githubusercontent.com/kubernetes-sigs/aws-load-balancer-controller/v2.11.0/docs/install/iam_policy.json
      aws iam create-policy \
        --policy-name AWSLoadBalancerControllerIAMPolicy \
        --policy-document file://iam_policy.json
    3. Create the IAM service account and attach the policy.

      eksctl create iamserviceaccount \
        --cluster=$EKS_CLUSTER_NAME \
        --namespace=kube-system \
        --name=aws-load-balancer-controller \
        --role-name AmazonEKSLoadBalancerControllerRole \
        --attach-policy-arn=arn:aws:iam::${AWS_ACCOUNT_ID}:policy/AWSLoadBalancerControllerIAMPolicy \
        --approve \
        --region $AWS_REGION
    4. Install the controller with Helm.

      helm repo add eks https://aws.github.io/eks-charts
      helm repo update eks
      helm install aws-load-balancer-controller eks/aws-load-balancer-controller \
        -n kube-system \
        --set clusterName=$EKS_CLUSTER_NAME \
        --set serviceAccount.create=false \
        --set serviceAccount.name=aws-load-balancer-controller
    5. Verify that the controller is running.

      kubectl -n kube-system rollout status deployment/aws-load-balancer-controller
  6. Enable AWS Outbound Identity Federation at the account level. The managed gateway that is provisioned as part of this guide requires this feature to validate JWT tokens from Keycloak via STS. To enable it, open the IAM Identity Center console and follow the prompts, or contact your AWS account administrator.

Step 1: Configure Keycloak

Solo Enterprise for agentregistry does not bundle its own identity provider. Instead, it relies on an external OIDC provider to authenticate users, issue access tokens, and enforce role-based access control. In this quickstart, you use Keycloak as your OIDC provider.

Before you deploy Keycloak, you configure it by creating a ConfigMap that defines the realm, clients, protocol mappers, groups, and users that Solo Enterprise for agentregistry requires. Keycloak reads this file at startup and automatically creates the following resources, so you do not have to configure them manually through the Keycloak UI.

ResourceNameDescription
RealmagentregistryA dedicated Keycloak realm that isolates agentregistry’s identity configuration from other applications. The registry server uses the realm’s issuer URL to discover OIDC endpoints.
Clientar-backendA confidential client that is used by the registry server to validate access tokens that are issued by the CLI and UI clients.
Clientar-cli-interactiveA public client for the arctl CLI. The client uses the device authorization grant (RFC 8628), which lets users log in from a terminal by opening a printed URL in a browser.
Clientar-cli-passwordA public client for scripted user authentication in CI/CD pipelines. Uses the password credentials grant with a username and password.
Clientar-uiA public client for the browser-based registry UI. This client uses the authorization code flow with PKCE. The redirectUris and webOrigins are set to * for local testing. In production deployments, these URLs must be replaced with the origin that your UI uses.
Protocol mappergroupsAn oidc-group-membership-mapper that writes the user’s Keycloak group memberships into the Groups JWT claim. The registry reads this claim to determine RBAC roles. The claim is added to access tokens that are issued for the ar-cli-interactive, ar-cli-password, and ar-ui clients.
Protocol mapperar-backend-audienceAn oidc-audience-mapper that adds ar-backend to the aud claim of every access token. Without the correct aud claim, the registry server rejects tokens with a 401 Unauthorized error, even when the user’s groups claim is correct, because the aud claim must match the backend client ID. The claim is added to access tokens that are issued for the ar-cli-interactive, ar-cli-password, and ar-ui clients.
GroupadminsA group that grants full admin access in the registry. Assigned to admin-user so they have superuser rights on first login.
Useradmin-userA test admin user with the password password, assigned to the admins group.
  1. Create the Keycloak namespace.

    kubectl create namespace keycloak
  2. Create the ConfigMap with your Solo Enterprise for agentregistry OIDC configuration.

    kubectl apply -f- <<EOF
    apiVersion: v1
    kind: ConfigMap
    metadata:
      name: keycloak-agentregistry-realm
      namespace: keycloak
    data:
      agentregistry-realm.json: |
        {
          "realm": "agentregistry",
          "enabled": true,
          "accessTokenLifespan": 86400,
          "ssoSessionIdleTimeout": 86400,
          "ssoSessionMaxLifespan": 86400,
          "clients": [
            {
              "clientId": "ar-backend",
              "enabled": true,
              "publicClient": false,
              "standardFlowEnabled": false,
              "directAccessGrantsEnabled": false
            },
            {
              "clientId": "ar-cli-interactive",
              "enabled": true,
              "publicClient": true,
              "standardFlowEnabled": false,
              "directAccessGrantsEnabled": false,
              "attributes": {
                "oauth2.device.authorization.grant.enabled": "true"
              },
              "protocolMappers": [
                {
                  "name": "groups",
                  "protocol": "openid-connect",
                  "protocolMapper": "oidc-group-membership-mapper",
                  "consentRequired": false,
                  "config": {
                    "claim.name": "Groups",
                    "full.path": "false",
                    "id.token.claim": "true",
                    "access.token.claim": "true",
                    "userinfo.token.claim": "true"
                  }
                },
                {
                  "name": "ar-backend-audience",
                  "protocol": "openid-connect",
                  "protocolMapper": "oidc-audience-mapper",
                  "consentRequired": false,
                  "config": {
                    "included.client.audience": "ar-backend",
                    "id.token.claim": "false",
                    "access.token.claim": "true"
                  }
                }
              ]
            },
            {
              "clientId": "ar-cli-password",
              "enabled": true,
              "publicClient": true,
              "standardFlowEnabled": false,
              "directAccessGrantsEnabled": true,
              "protocolMappers": [
                {
                  "name": "groups",
                  "protocol": "openid-connect",
                  "protocolMapper": "oidc-group-membership-mapper",
                  "consentRequired": false,
                  "config": {
                    "claim.name": "Groups",
                    "full.path": "false",
                    "id.token.claim": "true",
                    "access.token.claim": "true",
                    "userinfo.token.claim": "true"
                  }
                },
                {
                  "name": "ar-backend-audience",
                  "protocol": "openid-connect",
                  "protocolMapper": "oidc-audience-mapper",
                  "consentRequired": false,
                  "config": {
                    "included.client.audience": "ar-backend",
                    "id.token.claim": "false",
                    "access.token.claim": "true"
                  }
                }
              ]
            },
            {
              "clientId": "ar-ui",
              "enabled": true,
              "publicClient": true,
              "standardFlowEnabled": true,
              "directAccessGrantsEnabled": false,
              "redirectUris": ["*"],
              "webOrigins": ["*"],
              "attributes": {
                "pkce.code.challenge.method": "S256"
              },
              "protocolMappers": [
                {
                  "name": "groups",
                  "protocol": "openid-connect",
                  "protocolMapper": "oidc-group-membership-mapper",
                  "consentRequired": false,
                  "config": {
                    "claim.name": "Groups",
                    "full.path": "false",
                    "id.token.claim": "true",
                    "access.token.claim": "true",
                    "userinfo.token.claim": "true"
                  }
                },
                {
                  "name": "ar-backend-audience",
                  "protocol": "openid-connect",
                  "protocolMapper": "oidc-audience-mapper",
                  "consentRequired": false,
                  "config": {
                    "included.client.audience": "ar-backend",
                    "id.token.claim": "false",
                    "access.token.claim": "true"
                  }
                }
              ]
            }
          ],
          "groups": [
            {"name": "admins"}
          ],
          "users": [
            {
              "username": "admin-user",
              "email": "admin-user@example.com",
              "firstName": "Admin",
              "lastName": "User",
              "enabled": true,
              "credentials": [
                {"type": "password", "value": "password", "temporary": false}
              ],
              "groups": ["admins"]
            }
          ]
        }
    EOF

Step 2: Deploy Keycloak

With the realm configuration in place, you can now deploy Keycloak itself. This step creates the Keycloak pod and exposes it through a load balancer service so that both the registry server and the arctl CLI can reach it. The --import-realm flag instructs Keycloak to load the ConfigMap from Step 1 on startup to apply all the realm, client, and user configuration automatically.

  1. Create the Keycloak deployment and service from the configuration in your ConfigMap.

    kubectl apply -f- <<EOF
    apiVersion: v1
    kind: Service
    metadata:
      name: keycloak
      namespace: keycloak
      labels:
        app: keycloak
    spec:
      ports:
      - name: http
        port: 8080
        targetPort: 8080
      selector:
        app: keycloak
      type: LoadBalancer
    ---
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: keycloak
      namespace: keycloak
      labels:
        app: keycloak
    spec:
      replicas: 1
      selector:
        matchLabels:
          app: keycloak
      template:
        metadata:
          labels:
            app: keycloak
        spec:
          containers:
          - name: keycloak
            image: quay.io/keycloak/keycloak:26.1.3
            args: ["start-dev", "--import-realm"]
            env:
            - name: KEYCLOAK_ADMIN
              value: "admin"
            - name: KEYCLOAK_ADMIN_PASSWORD
              value: "admin"
            - name: PROXY_ADDRESS_FORWARDING
              value: "true"
            - name: KC_PROXY
              value: "edge"
            ports:
            - name: http
              containerPort: 8080
            readinessProbe:
              httpGet:
                path: /realms/master
                port: 8080
            volumeMounts:
            - name: realm-config
              mountPath: /opt/keycloak/data/import
          volumes:
          - name: realm-config
            configMap:
              name: keycloak-agentregistry-realm
    EOF
  2. Wait for the Keycloak rollout to finish.

    kubectl -n keycloak rollout status deploy/keycloak
  3. Set the Keycloak endpoint details from the load balancer service. These environment variables are used throughout the rest of this guide. The KEYCLOAK_ISSUER is the OIDC discovery URL that the registry uses to find the token and JWKS endpoints. KEYCLOAK_URL is the base URL that is needed to retrieve the ar-backend client secret in the next step.

    export ENDPOINT_KEYCLOAK=$(kubectl -n keycloak get service keycloak -o jsonpath='{.status.loadBalancer.ingress[0].ip}{.status.loadBalancer.ingress[0].hostname}'):8080
    export HOST_KEYCLOAK=$(echo ${ENDPOINT_KEYCLOAK} | cut -d: -f1)
    export PORT_KEYCLOAK=$(echo ${ENDPOINT_KEYCLOAK} | cut -d: -f2)
    export KEYCLOAK_URL=http://${ENDPOINT_KEYCLOAK}
    export KEYCLOAK_REALM=agentregistry
    export KEYCLOAK_ISSUER=$KEYCLOAK_URL/realms/${KEYCLOAK_REALM}
    echo $KEYCLOAK_ISSUER
  4. Get the client secret that Keycloak automatically generated for the ar-backend client. The registry server uses this secret to verify access tokens with Keycloak. After a user logs in with an access token, the registry server calls Keycloak’s introspection endpoint by using the ar-backend client secret to confirm that the token is valid and has not been tampered with. You pass this secret to the Solo Enterprise for agentregistry Helm chart in the next step.

    export KEYCLOAK_TOKEN=$(curl -s \
      -d "client_id=admin-cli" -d "username=admin" -d "password=admin" \
      -d "grant_type=password" \
      "${KEYCLOAK_URL}/realms/master/protocol/openid-connect/token" \
      | jq -r .access_token)
    
    export AR_BACKEND_ID=$(curl -s -H "Authorization: Bearer ${KEYCLOAK_TOKEN}" \
       "${KEYCLOAK_URL}/admin/realms/${KEYCLOAK_REALM}/clients?clientId=ar-backend" \
       | jq -r '.[0].id')
    
     export AR_BACKEND_SECRET=$(curl -s -H "Authorization: Bearer ${KEYCLOAK_TOKEN}" \
       "${KEYCLOAK_URL}/admin/realms/${KEYCLOAK_REALM}/clients/${AR_BACKEND_ID}/client-secret" \
       | jq -r .value)
     echo "AR_BACKEND_SECRET: $AR_BACKEND_SECRET"

Step 3: Install the arctl CLI

The arctl CLI is the primary tool for managing AI artifacts in Solo Enterprise for agentregistry. You use it to create, publish, and deploy AI artifacts. Before you can run any arctl commands, you must authenticate the CLI with Keycloak to obtain a valid access token.

  1. Download and install the arctl CLI.

    curl -sSL https://storage.googleapis.com/agentregistry-enterprise/install.sh | ARCTL_VERSION=v2026.7.0 sh
    export PATH=$HOME/.arctl/bin:$PATH
  2. Log in to the registry. Choose between the interactive device authorization and the static username and password credential flow.

    Use this flow for interactive logins from a terminal. The CLI uses the ar-cli-interactive public client and starts a device authorization grant flow that prints a URL and a one-time code for you to approve in a browser.

    1. Configure the CLI to use the Keycloak issuer and ar-cli-interactive client. Then, log in to Solo Enterprise for agentregistry.

      export OIDC_ISSUER=$KEYCLOAK_ISSUER
      export OIDC_CLIENT_ID=ar-cli-interactive
      arctl user login

      Example output:

      To complete the login process, please:
       1. Open: http://172.18.0.13:8080/realms/agentregistry/device
       2. Enter the code: SYSQ-SNSP
       - or go to: http://172.18.0.13:8080/realms/agentregistry/device?user_code=SYSQ-SNSP
      
    2. Open the URL in a web browser and log into Keycloak with the admin-user username and password password. Confirming the code in the browser tells Keycloak to issue an access token for the CLI session. After authorization is complete, the CLI stores the token in your system keychain so you do not have to log in again until it expires.

    Use this flow for scripted logins where a specific user identity is required in CI/CD pipelines. The CLI uses the ar-cli-password public client and exchanges a username and password directly for an access token.

    1. Log in to Solo Enterprise for agentregistry with the admin-user/password credentials.

      export OIDC_ISSUER=$KEYCLOAK_ISSUER
      arctl user login \
        --oidc-flow=password-credentials \
        --oidc-client-id=ar-cli-password \
        --oidc-username=admin-user \
        --oidc-password=password
    2. Save the access token in the ARCTL_API_TOKEN environment variable.

      export ARCTL_API_TOKEN=$(arctl user info --show-tokens | jq -r .access_token)

  3. Verify that you can run arctl commands.

    arctl user whoami

    Example output:

    FIELD       VALUE
    Subject     8d7ffd9c-1f75-459a-b900-bfa4f5e3b492
    Email       admin-user@example.com
    Issuer      http://172.18.0.10:8080/realms/agentregistry
    Superuser   true (overrides role-based permission checks)
    
    ROLE     STATUS
    admins   no explicit permissions configured
    
    If you see 401 errors in the CLI, you might have previously stored a token in the ARCTL_API_TOKEN environment variable. This environment variable takes precedence over the token that you obtain via the CLI login flow. Try running unset ARCTL_API_TOKEN. Then, repeat the CLI login flow.

Step 4: Prepare the AWS environment

To deploy agents and MCP servers to AWS Bedrock AgentCore, Solo Enterprise for agentregistry needs permission to act in your AWS account on your behalf. In this step, you:

  • Grant AWS access to the registry server: You can choose between two authentication methods to grant the registry server access to your AWS account so that the server can deploy and manage AWS Bedrock AgentCore resources on your behalf.
    • IAM user credentials: Use static long-lived access keys.
    • EKS Pod Identity: Use short-lived credentials that are automatically rotated by EKS.
  • Create a cross-account IAM role: You generate a CloudFormation template and deploy it into your account. The template creates an IAM role that Solo Enterprise for agentregistry assumes when it deploys or manages resources in AWS Bedrock AgentCore.
  • Register the runtime: You register the AWS runtime in Solo Enterprise for agentregistry.
  1. Configure the AWS CLI with credentials for an IAM user that has permission to create IAM users, roles, policies, and access keys. Run the following command and enter your AWS Access Key ID, Secret Access Key, and default region when prompted.

    aws configure
  2. Set your AWS Region and account ID as environment variables.

    export AWS_REGION=<your-aws-region>
    export AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
    echo "AWS_ACCOUNT_ID: $AWS_ACCOUNT_ID"
  3. Create the IAM policy documents and managed policies. These policies grant the registry server and Bedrock AgentCore the permissions they need. You attach these policies to either an IAM user or an IAM role in the next step, depending on the authentication method that you choose.

    cat > general-access-policy.json << 'EOF'
    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Effect": "Allow",
          "NotAction": [
            "iam:*",
            "organizations:*",
            "account:*"
          ],
          "Resource": "*"
        },
        {
          "Effect": "Allow",
          "Action": [
            "account:GetAccountInformation",
            "account:GetGovCloudAccountInformation",
            "account:GetPrimaryEmail",
            "account:ListRegions",
            "iam:CreateServiceLinkedRole",
            "iam:DeleteServiceLinkedRole",
            "iam:ListRoles",
            "organizations:DescribeEffectivePolicy",
            "organizations:DescribeOrganization"
          ],
          "Resource": "*"
        }
      ]
    }
    EOF
    The general-access-policy.json uses a broad NotAction statement that allows all AWS actions except IAM, Organizations, and Account management. This includes the EC2 permissions (ec2:RunInstances, ec2:DescribeInstances, ec2:TerminateInstances, and related) that are required later in this guide to provision the managed gateway’s EC2 instance.
    cat > bedrock-agentcore-policy.json << 'EOF'
    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Sid": "BedrockAgentCoreFullAccess",
          "Effect": "Allow",
          "Action": ["bedrock-agentcore:*"],
          "Resource": "arn:aws:bedrock-agentcore:*:*:*"
        },
        {
          "Sid": "IAMListAccess",
          "Effect": "Allow",
          "Action": [
            "iam:GetRole",
            "iam:GetRolePolicy",
            "iam:ListAttachedRolePolicies",
            "iam:ListRolePolicies",
            "iam:ListRoles"
          ],
          "Resource": "arn:aws:iam::*:role/*"
        },
        {
          "Sid": "BedrockAgentCorePassRoleAccess",
          "Effect": "Allow",
          "Action": "iam:PassRole",
          "Resource": "arn:aws:iam::*:role/*BedrockAgentCore*",
          "Condition": {
            "StringEquals": {
              "iam:PassedToService": "bedrock-agentcore.amazonaws.com"
            }
          }
        },
        {
          "Sid": "SecretsManagerAccess",
          "Effect": "Allow",
          "Action": [
            "secretsmanager:CreateSecret",
            "secretsmanager:PutSecretValue",
            "secretsmanager:GetSecretValue",
            "secretsmanager:DeleteSecret"
          ],
          "Resource": "arn:aws:secretsmanager:*:*:secret:bedrock-agentcore*"
        },
        {
          "Sid": "BedrockAgentCoreKMSReadAccess",
          "Effect": "Allow",
          "Action": ["kms:ListKeys", "kms:DescribeKey"],
          "Resource": ["arn:aws:kms:*:*:key/*"],
          "Condition": {
            "StringEquals": {
              "aws:ResourceAccount": "${aws:PrincipalAccount}"
            }
          }
        },
        {
          "Sid": "BedrockAgentCoreKMSAccess",
          "Effect": "Allow",
          "Action": ["kms:Decrypt", "kms:GenerateDataKey", "kms:ListGrants"],
          "Resource": ["arn:aws:kms:*:*:key/*"],
          "Condition": {
            "StringEquals": {
              "aws:ResourceAccount": "${aws:PrincipalAccount}"
            },
            "ForAnyValue:StringEquals": {
              "aws:CalledVia": ["bedrock-agentcore.amazonaws.com"]
            }
          }
        },
        {
          "Sid": "BedrockAgentCoreKMSGrantsAccess",
          "Effect": "Allow",
          "Action": ["kms:CreateGrant"],
          "Resource": ["arn:aws:kms:*:*:key/*"],
          "Condition": {
            "StringEquals": {
              "kms:GrantConstraintType": "EncryptionContextSubset"
            },
            "StringLike": {
              "kms:ViaService": ["bedrock-agentcore.*.amazonaws.com"],
              "kms:EncryptionContext:aws:bedrock-agentcore-gateway:arn": "arn:aws:bedrock-agentcore:*:*:gateway/*"
            },
            "ForAllValues:StringEquals": {
              "kms:GrantOperations": ["Decrypt", "GenerateDataKey"]
            }
          }
        },
        {
          "Sid": "BedrockAgentCoreS3Access",
          "Effect": "Allow",
          "Action": ["s3:GetObject"],
          "Resource": ["arn:aws:s3:::bedrock-agentcore-gateway-*"],
          "Condition": {
            "StringEquals": {
              "aws:CalledViaLast": "bedrock-agentcore.amazonaws.com",
              "s3:ResourceAccount": "${aws:PrincipalAccount}"
            }
          }
        },
        {
          "Sid": "BedrockAgentCoreGatewayLambdaAccess",
          "Effect": "Allow",
          "Action": ["lambda:ListFunctions"],
          "Resource": ["arn:aws:lambda:*:*:*"]
        },
        {
          "Sid": "BedrockAgentCoreGatewayApiGateway",
          "Effect": "Allow",
          "Action": ["apigateway:GET"],
          "Resource": ["arn:aws:apigateway:*::/restapis/*/stages/*/exports/*"]
        },
        {
          "Sid": "LoggingAccess",
          "Effect": "Allow",
          "Action": [
            "logs:Get*",
            "logs:List*",
            "logs:StartQuery",
            "logs:StopQuery",
            "logs:Describe*",
            "logs:TestMetricFilter",
            "logs:FilterLogEvents"
          ],
          "Resource": [
            "arn:aws:logs:*:*:log-group:/aws/bedrock-agentcore/*",
            "arn:aws:logs:*:*:log-group:/aws/application-signals/data:*",
            "arn:aws:logs:*:*:log-group:aws/spans:*"
          ]
        },
        {
          "Sid": "ObservabilityReadOnlyPermissions",
          "Effect": "Allow",
          "Action": [
            "application-autoscaling:DescribeScalingPolicies",
            "application-signals:BatchGet*",
            "application-signals:Get*",
            "application-signals:List*",
            "autoscaling:Describe*",
            "cloudwatch:BatchGet*",
            "cloudwatch:Describe*",
            "cloudwatch:GenerateQuery",
            "cloudwatch:Get*",
            "cloudwatch:List*",
            "oam:ListSinks",
            "rum:BatchGet*",
            "rum:Get*",
            "rum:List*",
            "synthetics:Describe*",
            "synthetics:Get*",
            "synthetics:List*",
            "xray:BatchGet*",
            "xray:Get*",
            "xray:List*",
            "xray:StartTraceRetrieval",
            "xray:CancelTraceRetrieval",
            "logs:DescribeLogGroups",
            "logs:StartLiveTail",
            "logs:StopLiveTail"
          ],
          "Resource": "*"
        },
        {
          "Sid": "TransactionSearchXRayPermissions",
          "Effect": "Allow",
          "Action": [
            "xray:GetTraceSegmentDestination",
            "xray:UpdateTraceSegmentDestination",
            "xray:GetIndexingRules",
            "xray:UpdateIndexingRule"
          ],
          "Resource": "*"
        },
        {
          "Sid": "TransactionSearchLogGroupPermissions",
          "Effect": "Allow",
          "Action": [
            "logs:CreateLogGroup",
            "logs:CreateLogStream",
            "logs:PutRetentionPolicy"
          ],
          "Resource": [
            "arn:aws:logs:*:*:log-group:/aws/application-signals/data:*",
            "arn:aws:logs:*:*:log-group:aws/spans:*"
          ]
        },
        {
          "Sid": "TransactionSearchLogsPermissions",
          "Effect": "Allow",
          "Action": [
            "logs:DescribeResourcePolicies",
            "logs:PutResourcePolicy"
          ],
          "Resource": ["*"],
          "Condition": {
            "StringEquals": {
              "aws:ResourceAccount": "${aws:PrincipalAccount}"
            }
          }
        },
        {
          "Sid": "TransactionSearchApplicationSignalsPermissions",
          "Effect": "Allow",
          "Action": ["application-signals:StartDiscovery"],
          "Resource": "*"
        },
        {
          "Sid": "CloudWatchApplicationSignalsCreateServiceLinkedRolePermissions",
          "Effect": "Allow",
          "Action": "iam:CreateServiceLinkedRole",
          "Resource": "arn:aws:iam::*:role/aws-service-role/application-signals.cloudwatch.amazonaws.com/AWSServiceRoleForCloudWatchApplicationSignals",
          "Condition": {
            "StringLike": {
              "iam:AWSServiceName": "application-signals.cloudwatch.amazonaws.com"
            }
          }
        },
        {
          "Sid": "CloudWatchApplicationSignalsGetRolePermissions",
          "Effect": "Allow",
          "Action": "iam:GetRole",
          "Resource": "arn:aws:iam::*:role/aws-service-role/application-signals.cloudwatch.amazonaws.com/AWSServiceRoleForCloudWatchApplicationSignals"
        },
        {
          "Sid": "CreateBedrockAgentCoreNetworkServiceLinkedRolePermissions",
          "Effect": "Allow",
          "Action": "iam:CreateServiceLinkedRole",
          "Resource": "arn:aws:iam::*:role/aws-service-role/network.bedrock-agentcore.amazonaws.com/AWSServiceRoleForBedrockAgentCoreNetwork",
          "Condition": {
            "StringEquals": {
              "iam:AWSServiceName": "network.bedrock-agentcore.amazonaws.com"
            }
          }
        },
        {
          "Sid": "CreateBedrockAgentCoreRuntimeIdentityServiceLinkedRolePermissions",
          "Effect": "Allow",
          "Action": "iam:CreateServiceLinkedRole",
          "Resource": "arn:aws:iam::*:role/aws-service-role/runtime-identity.bedrock-agentcore.amazonaws.com/AWSServiceRoleForBedrockAgentCoreRuntimeIdentity",
          "Condition": {
            "StringEquals": {
              "iam:AWSServiceName": "runtime-identity.bedrock-agentcore.amazonaws.com"
            }
          }
        },
        {
          "Sid": "CloudWatchApplicationSignalsCloudTrailPermissions",
          "Effect": "Allow",
          "Action": ["cloudtrail:CreateServiceLinkedChannel"],
          "Resource": "arn:aws:cloudtrail:*:*:channel/aws-service-channel/application-signals/*"
        },
        {
          "Sid": "BedrockAgentCoreRuntimeS3WriteAccess",
          "Effect": "Allow",
          "Action": [
            "s3:CreateBucket",
            "s3:PutBucketPolicy",
            "s3:PutBucketVersioning",
            "s3:PutObject"
          ],
          "Resource": ["arn:aws:s3:::bedrock-agentcore-runtime-*"],
          "Condition": {
            "StringEquals": {
              "s3:ResourceAccount": "${aws:PrincipalAccount}"
            }
          }
        },
        {
          "Sid": "BedrockAgentCoreRuntimeS3ReadAccess",
          "Effect": "Allow",
          "Action": [
            "s3:GetObject",
            "s3:GetObjectVersion",
            "s3:ListBucket",
            "s3:ListBucketVersions"
          ],
          "Resource": "arn:aws:s3:::*",
          "Condition": {
            "StringEquals": {
              "s3:ResourceAccount": "${aws:PrincipalAccount}"
            }
          }
        },
        {
          "Sid": "BedrockAgentCoreRuntimeS3ListAccess",
          "Effect": "Allow",
          "Action": ["s3:ListAllMyBuckets"],
          "Resource": "*",
          "Condition": {
            "StringEquals": {
              "s3:ResourceAccount": "${aws:PrincipalAccount}"
            }
          }
        },
        {
          "Sid": "BedrockAgentCoreRuntimeECRAccess",
          "Effect": "Allow",
          "Action": [
            "ecr:DescribeRepositories",
            "ecr:DescribeImages",
            "ecr:ListImages"
          ],
          "Resource": ["arn:aws:ecr:*:*:repository/*"]
        },
        {
          "Sid": "AgentCoreEvaluationCloudWatchLogCreate",
          "Effect": "Allow",
          "Action": ["logs:CreateLogGroup"],
          "Resource": ["arn:aws:logs:*:*:log-group:/aws/bedrock-agentcore/evaluations/*"]
        },
        {
          "Sid": "AgentCoreEvaluationCloudWatchLogIndexAccess",
          "Effect": "Allow",
          "Action": [
            "logs:PutIndexPolicy",
            "logs:DescribeIndexPolicies"
          ],
          "Resource": [
            "arn:aws:logs:*:*:log-group:aws/spans",
            "arn:aws:logs:*:*:log-group:aws/spans:*"
          ]
        },
        {
          "Sid": "AgentCoreEvaluationBedrockInvokeAccess",
          "Effect": "Allow",
          "Action": [
            "bedrock:InvokeModel",
            "bedrock:InvokeModelWithResponseStream"
          ],
          "Resource": [
            "arn:aws:bedrock:*::foundation-model/*",
            "arn:aws:bedrock:*:*:inference-profile/*"
          ]
        },
        {
          "Sid": "AgentCoreEvaluationLambdaAccess",
          "Effect": "Allow",
          "Action": [
            "lambda:InvokeFunction",
            "lambda:GetFunction"
          ],
          "Resource": "arn:aws:lambda:*:*:function:*"
        }
      ]
    }
    EOF

    Create the managed policies in AWS IAM. Customer-managed policies support up to 6,144 characters and can be attached to multiple IAM principals without duplication.

    aws iam create-policy \
      --policy-name AgentRegistryGeneralAccess \
      --policy-document file://general-access-policy.json
    
    aws iam create-policy \
      --policy-name AgentRegistryBedrockAgentCoreAccess \
      --policy-document file://bedrock-agentcore-policy.json
  4. Choose your AWS authentication method. The registry server uses these credentials whenever it deploys or manages Bedrock AgentCore resources on your behalf.

    Use this method to create a dedicated IAM user and attach long-lived access key credentials. The registry server stores the access key ID and secret in a Kubernetes Secret and uses them to call AWS APIs.

    1. Create a dedicated IAM user for Solo Enterprise for agentregistry.

      aws iam create-user --user-name agentregistry-deployer
    2. Attach the general access policy to the user.

      aws iam attach-user-policy \
        --user-name agentregistry-deployer \
        --policy-arn arn:aws:iam::$AWS_ACCOUNT_ID:policy/AgentRegistryGeneralAccess
    3. Attach the Bedrock AgentCore access policy to the user.

      aws iam attach-user-policy \
        --user-name agentregistry-deployer \
        --policy-arn arn:aws:iam::$AWS_ACCOUNT_ID:policy/AgentRegistryBedrockAgentCoreAccess
    4. Create an access key for the agentregistry-deployer user and export the credentials as environment variables.

      ACCESS_KEY_OUTPUT=$(aws iam create-access-key --user-name agentregistry-deployer)
      export AWS_ACCESS_KEY_ID=$(echo $ACCESS_KEY_OUTPUT | jq -r '.AccessKey.AccessKeyId')
      export AWS_SECRET_ACCESS_KEY=$(echo $ACCESS_KEY_OUTPUT | jq -r '.AccessKey.SecretAccessKey')
      echo "AWS_ACCESS_KEY_ID: $AWS_ACCESS_KEY_ID"

    Use this method to attach an IAM role to the registry server’s Kubernetes service account by using EKS Pod Identity. The EKS Pod Identity agent automatically injects short-lived, rotating AWS credentials into the registry pod. No long-lived access keys are stored.

    1. Install the EKS Pod Identity Agent into your EKS cluster.

      aws eks create-addon --cluster-name $EKS_CLUSTER_NAME --addon-name eks-pod-identity-agent --region $AWS_REGION
    2. Create a trust policy document that allows the EKS Pod Identity service to assume the role.

      cat > pod-identity-trust-policy.json << 'EOF'
      {
        "Version": "2012-10-17",
        "Statement": [
          {
            "Effect": "Allow",
            "Principal": {
              "Service": "pods.eks.amazonaws.com"
            },
            "Action": [
              "sts:AssumeRole",
              "sts:TagSession"
            ]
          }
        ]
      }
      EOF
    3. Create the IAM role with the trust policy.

      aws iam create-role \
        --role-name agentregistry-pod-identity-role \
        --assume-role-policy-document file://pod-identity-trust-policy.json
      export POD_IDENTITY_ROLE_ARN=$(aws iam get-role \
        --role-name agentregistry-pod-identity-role \
        --query 'Role.Arn' --output text)
      echo "POD_IDENTITY_ROLE_ARN: $POD_IDENTITY_ROLE_ARN"
    4. Attach the general access policy to the role.

      aws iam attach-role-policy \
        --role-name agentregistry-pod-identity-role \
        --policy-arn arn:aws:iam::$AWS_ACCOUNT_ID:policy/AgentRegistryGeneralAccess
    5. Attach the Bedrock AgentCore access policy to the role.

      aws iam attach-role-policy \
        --role-name agentregistry-pod-identity-role \
        --policy-arn arn:aws:iam::$AWS_ACCOUNT_ID:policy/AgentRegistryBedrockAgentCoreAccess
    6. Create the Pod Identity Association. This binds the IAM role to the agentregistry-enterprise service account in the agentregistry-system namespace. The EKS Pod Identity agent uses this binding to inject temporary credentials into the registry pod.

      aws eks create-pod-identity-association \
        --cluster-name $EKS_CLUSTER_NAME \
        --namespace agentregistry-system \
        --service-account agentregistry-enterprise \
        --role-arn $POD_IDENTITY_ROLE_ARN \
        --region $AWS_REGION
  5. Generate the AWS CloudFormation template and save it in a YAML file. The arctl runtime setup aws command generates a resource stack template that is scoped to your account ID. When you deploy the template, it creates an IAM role that Solo Enterprise for agentregistry assumes to deploy and manage Bedrock AgentCore resources on your behalf, along with all required IAM policies. With CloudFormation, you get a reviewable, auditable definition of all the resources that are created in your account.

    arctl runtime setup bedrock-agent-core --aws-account-id $AWS_ACCOUNT_ID > cloudformation.yaml

    The External ID and Role Name are printed to your CLI output and are also embedded in the cloudformation.yaml file. However, the External ID parameter is marked NoEcho: true, which means AWS masks it when you describe the stack or view it in the console. Make sure to note the External ID from the CLI output now so you have it later.

    Example output:

    # External ID: NzqfvveAy0XSEsL4ZtKcHR1li-3puJAbipQDQG8ylmg
    # Role Name: AgentRegistryAccessRole-a11111
  6. Save the External ID in an environment variable. Replace <external-id> with the value that you retrieved in the previous step.

    export AWS_EXTERNAL_ID="<external-id>"
  7. Optional: Review the template.

    cat cloudformation.yaml
  8. Use the aws CLI to deploy the resource stack to your account.

    1. Choose a name for your AWS stack, such as agentregistry-cli-stack.

      export STACK_NAME="<stack-name>"
    2. Create the AWS stack. The --capabilities CAPABILITY_NAMED_IAM flag is required, because the template creates an IAM role, and AWS requires explicit acknowledgment when a CloudFormation template creates IAM resources. If the command succeeds, you see a StackId and OperationId in your CLI output.

      aws cloudformation create-stack \
       --stack-name $STACK_NAME \
       --template-body file://cloudformation.yaml \
       --region $AWS_REGION \
       --capabilities CAPABILITY_NAMED_IAM

      Example output:

      {
        "StackId": "arn:aws:cloudformation:us-west-2:1234567890:stack/agentregistry-cli-stack/1a1aaa11-1111-11a1-aa11-1111a1111aa1",
       "OperationId": "1aaa1a1a-1111-11aa-1a1a-1aaa11a1a111"
      }
      
    3. Wait for the stack to finish deploying. The command exits when the stack status is CREATE_COMPLETE.

      aws cloudformation wait stack-create-complete \
       --stack-name $STACK_NAME \
       --region $AWS_REGION
  9. Store the RoleArn of the IAM role that was created by the CloudFormation template in an environment variable. Solo Enterprise for agentregistry assumes this role to deploy and manage Bedrock AgentCore resources in your AWS account.

    export AWS_ROLE_ARN=$(aws cloudformation describe-stacks \
    --stack-name $STACK_NAME \
    --query 'Stacks[0].Outputs[?OutputKey==`RoleArn`].OutputValue' \
    --output text \
    --region $AWS_REGION)
    
    echo $AWS_ROLE_ARN

Step 5: Install Solo Enterprise for agentregistry

Install Solo Enterprise for agentregistry and connect it to Keycloak and your AWS account. Choose the tab that matches the authentication method you configured in the previous step.

  1. Save your Solo Enterprise for agentregistry license key in an environment variable. To obtain the key, contact an account representative. For more options, such as providing the key in a secret, see Licensing.

    export AGENTREGISTRY_LICENSE_KEY=<license_key>
  2. Install Solo Enterprise for agentregistry.

    helm upgrade --install agentregistry \
      oci://us-docker.pkg.dev/solo-public/agentregistry-enterprise/helm/agentregistry-enterprise \
      --version 2026.7.0 \
      --namespace agentregistry-system \
      --create-namespace \
      --set licensing.createSecret=true \
      --set licensing.licenseKey=$AGENTREGISTRY_LICENSE_KEY \
      --set oidc.issuer=$KEYCLOAK_ISSUER \
      --set oidc.clientId=ar-backend \
      --set oidc.clientSecret=$AR_BACKEND_SECRET \
      --set oidc.publicClientId=ar-ui \
      --set oidc.roleClaim=Groups \
      --set oidc.superuserRole=admins \
      --set aws.enabled=true \
      --set-string aws.accountId="${AWS_ACCOUNT_ID}" \
      --set-string aws.region="${AWS_REGION}" \
      --set-string aws.accessKeyId="${AWS_ACCESS_KEY_ID}" \
      --set-string aws.secretAccessKey="${AWS_SECRET_ACCESS_KEY}" \
      --set-string "telemetry.service.annotations.service\.beta\.kubernetes\.io/aws-load-balancer-nlb-target-type=ip" \
      --set-string "telemetry.service.annotations.service\.beta\.kubernetes\.io/aws-load-balancer-scheme=internal" \
      --set-string "telemetry.service.annotations.service\.beta\.kubernetes\.io/aws-load-balancer-type=external" \
      --set telemetry.service.type=LoadBalancer
    helm upgrade --install agentregistry \
      oci://us-docker.pkg.dev/solo-public/agentregistry-enterprise/helm/agentregistry-enterprise \
      --version 2026.7.0 \
      --namespace agentregistry-system \
      --create-namespace \
      --set licensing.createSecret=true \
      --set licensing.licenseKey=$AGENTREGISTRY_LICENSE_KEY \
      --set oidc.issuer=$KEYCLOAK_ISSUER \
      --set oidc.clientId=ar-backend \
      --set oidc.clientSecret=$AR_BACKEND_SECRET \
      --set oidc.publicClientId=ar-ui \
      --set oidc.roleClaim=Groups \
      --set oidc.superuserRole=admins \
      --set aws.enabled=true \
      --set aws.usePodIdentity=true \
      --set-string aws.region="${AWS_REGION}" \
      --set-string "telemetry.service.annotations.service\.beta\.kubernetes\.io/aws-load-balancer-nlb-target-type=ip" \
      --set-string "telemetry.service.annotations.service\.beta\.kubernetes\.io/aws-load-balancer-scheme=internal" \
      --set-string "telemetry.service.annotations.service\.beta\.kubernetes\.io/aws-load-balancer-type=external" \
      --set telemetry.service.type=LoadBalancer
    Helm valueDescription
    licensing.createSecretSet to true so that the chart creates the license secret from the licensing.licenseKey value.
    licensing.licenseKeyYour Solo Enterprise for agentregistry license key.
    oidc.issuerThe issuer URL of your Keycloak realm. The registry uses this URL to discover the OIDC provider’s endpoints.
    oidc.clientIdThe confidential backend client ID (ar-backend).
    oidc.clientSecretThe client secret for the ar-backend client.
    oidc.publicClientIdThe public client ID for the browser-based UI (ar-ui). The server uses this client to log in to the UI.
    oidc.roleClaimThe JWT claim that you want to use to extract the role or group name. In this example, the claim must match the Token Claim Name in the group mapper (Groups) that you configured in Keycloak.
    oidc.superuserRoleThe role name that grants full admin access (admins).
    aws.enabledEnables the AWS integration in the registry server.
    aws.accessKeyId / aws.secretAccessKeyIAM user credentials for the agentregistry-deployer user. Use these fields only if you chose the IAM user credential path earlier. The registry server uses the static IAM credentials when deploying agents and MCP servers to AWS Bedrock AgentCore.
    aws.usePodIdentityWhen true, the chart skips creating an AWS credentials Secret and relies on credentials that are injected by the EKS Pod Identity agent. Use this field only if you chose to authenticate via the EKS Pod Identity agent.
    aws.regionThe AWS region where Bedrock AgentCore resources are deployed.
    telemetry.service.annotationsAnnotations to apply to the telemetry service. In this example, the annotations configure an internal AWS Network Load Balancer (NLB) with IP-based target routing.
    telemetry.service.typeExposes the OTel collector as a LoadBalancer service so that deployed agent and MCP server runtimes can reach it from outside the cluster.
  3. Verify that the Solo Enterprise for agentregistry pods are up and running.

    kubectl get pods -n agentregistry-system

    Example output:

    NAME                                                            READY   STATUS    RESTARTS   AGE
    agentregistry-enterprise-server-76f8fcc656-bmfsr                1/1     Running   0          2m21s
    agentregistry-enterprise-postgresql-764898cbff-5b5tn            1/1     Running   0          2m21s
    agentregistry-enterprise-telemetry-collector-777798c5b4-d9cwt   1/1     Running   0          2m21s
    agentregistry-clickhouse-shard0-0                               1/1     Running   0          2m21s
    
  4. Port-forward the Solo Enterprise for agentregistry controller on port 12121. The registry service is not exposed externally by default, so port-forwarding is required to reach it from your local machine. The arctl CLI and the UI both default to http://localhost:12121 as the registry endpoint. To expose the registry server on a Load Balancer service, add the --set service.type=LoadBalancer setting to your Helm installation.

    kubectl -n agentregistry-system port-forward svc/agentregistry-enterprise-server 12121:12121
  5. Open the Solo Enterprise for agentregistry UI. Log in to Keycloak with the admin-user username and password password. Verify that you see the dashboard.

Step 6: Register the AgentCore runtime

Register the AWS Bedrock AgentCore runtime with Solo Enterprise for agentregistry. The runtime connects the registry server to your AWS account so that agents and MCP servers can be deployed to Bedrock AgentCore.

  1. Get the OpenTelemetry collector load balancer hostname. The registry deploys an OTel collector alongside the control plane. Agent and MCP server runtimes use this endpoint to export traces and metrics.

    export OTEL_HOST=$(kubectl -n agentregistry-system get svc \
      agentregistry-enterprise-telemetry-collector \
      -o jsonpath='{.status.loadBalancer.ingress[0].hostname}')
    echo "OTel endpoint: http://${OTEL_HOST}:4318"
  2. Register the AWS Bedrock AgentCoreruntime.

    arctl apply -f- <<EOF
    apiVersion: ar.dev/v1alpha1
    kind: Runtime
    metadata:
      name: agentcore
    spec:
      type: BedrockAgentCore
      telemetryEndpoint: http://${OTEL_HOST}:4318
      config:
        roleArn: $AWS_ROLE_ARN
        externalId: $AWS_EXTERNAL_ID
        region: $AWS_REGION
    EOF

    Example output:

    ✓ Runtime/agentcore created
    
  3. List the runtimes that are connected to Solo Enterprise for agentregistry and verify that the Bedrock AgentCore runtime is listed.

    arctl get runtimes

    Example output:

    NAME                 TYPE
    agentcore            BedrockAgentCore
    kubernetes-default   kubernetes
    local                local
    

Step 7: Create the managed gateway

The managed gateway is a key component of the Solo Enterprise for agentregistry deployment. It provisions an EC2 instance in your VPC and installs the Solo Enterprise for agentgateway binary on it. All traffic between deployed agents and MCP servers is routed through this gateway, which allows the registry to enforce access policies and apply observability to every interaction.

Why do you need a gateway?

Bedrock AgentCore runtimes are isolated compute environments. When an agent calls an MCP server, that call must stay within your private VPC and cannot traverse the public internet. The managed gateway is deployed into the same VPC that your agents and MCP servers run in and proxies all traffic between the agent and the MCP server. Without the gateway, a Bedrock AgentCore agent cannot access tools that are exposed on an MCP server that is also deployed to AgentCore.

The gateway also enforces AccessPolicies to control whether or not an agent can call other agents or access MCP server tools during runtime.

  1. Set the Keycloak JWKS endpoint. The gateway uses this URL to fetch the public keys it needs to validate JWT tokens.

    export JWKS_URL="${KEYCLOAK_ISSUER}/protocol/openid-connect/certs"
    echo "JWKS URL: ${JWKS_URL}"
  2. Create the managed gateway. The gateway is provisioned in the private subnet you identified in Step 6 and attached to the default security group. The STS configuration tells the gateway to validate access tokens against your Keycloak JWKS endpoint and to pass the Groups claim through for downstream policy evaluation.

    arctl apply -f- <<EOF
    apiVersion: ar.dev/v1alpha1
    kind: Gateway
    metadata:
      name: gateway-agentcore
    spec:
      runtimeId: agentcore
      networkId: "${VPC_ID}"
      subnetId: "${SUBNET_ID}"
      mode: managed
      sts:
        allowedSubjectClaims: 
        - Groups
        subjectValidator:
          remote: "${JWKS_URL}"
      aws:
        agentCoreRuntimeSecurityGroupIds:
        - "${SG_ID}"
    EOF

    Example output:

    ✓ Gateway/gateway-agentcore created
    
  3. Wait for the gateway to reach phase=ready. Provisioning the EC2 instance and installing the agentgateway binary typically takes 5–10 minutes to complete.

    arctl get gateways

    Repeat the command every minute until you see ready in the PHASE column:

    NAME               PROVIDER   MODE      PHASE
    gateway-agentcore  agentcore  managed   ready
    
    If the gateway reaches phase=failed, check the gateway details for an error message: arctl get gateway gateway-agentcore -o yaml.

Step 8: Scaffold and publish an MCP server

In this step, you create a Python MCP server by using the fastmcp framework. The server exposes an echo and sum tool that your agent can call during a conversation. You publish it to the Solo Enterprise for agentregistry catalog, where it becomes discoverable and can be attached to any agent.

  1. Scaffold the MCP server.

    The following command creates a mymcp directory that contains the scaffold for your MCP server, including a src/main.py entry point and a Dockerfile.

    arctl init mcp mymcp --framework fastmcp --language python \
      --description "Sample MCP server" \
      --image localhost:5001/mymcp:latest

    Example output:

    ✓ Created MCP server: mymcp (framework: fastmcp, language: python)
    
  2. Explore the MCP server scaffold.

    ls -R mymcp

    Example output:

    Dockerfile     mcp.yaml       pyproject.toml README.md      src            tests
    
    mymcp/src:
    core    main.py tools
    
    mymcp/src/core:
    __init__.py server.py   utils.py
    
    mymcp/src/tools:
    __init__.py echo.py sum.py
    
    mymcp/tests:
    test_discovery.py test_server.py    test_tools.py
    
    FileDescription
    arctl.yamlThe MCP server details that you specified with the arctl CLI.
    DockerfileThe Dockerfile to spin up and run your MCP server in a containerized environment.
    mcp.yamlThe MCP server configuration file that defines server metadata, transport settings, version, and other server-specific configuration.
    pyproject.tomlThe Python project configuration file that defines project dependencies, build settings, and metadata for the MCP server.
    README.mdAn introduction to the MCP server that you created with instructions for how to further customize it.
    srcA directory that contains the details of the MCP server, such as supported tools and the Python script to bootstrap and run the server.
    src.coreA directory that contains the MCP server source code files.
    src.toolsA directory that defines the tools that the MCP server can use. The sample scaffold includes an echo and a sum tool.
    testsA directory that contains generated tests.
  3. Store the MCP server source code in a GitHub repository.

    Each runtime packages the MCP server differently. Solo Enterprise for agentregistry fetches the source code from a GitHub repository at deploy time and packages it for the target runtime.

    This tutorial uses the https://github.com/solo-io/doc-examples Solo.io GitHub repository so that you do not have to push your MCP server code to a GitHub repository. Simply continue with the next step to publish the MCP server. If you already have an MCP server in a GitHub repository, you can use that repository URL instead.
  4. Edit the mcp.yaml file to add the GitHub repository information. This tells Solo Enterprise for agentregistry where to fetch the source code when deploying the MCP server.

    cat << EOF > mymcp/mcp.yaml
    apiVersion: ar.dev/v1alpha1
    kind: MCPServer
    metadata:
      name: mymcp
    spec:
      description: mymcp MCP server
      source:
        package:
          launch:
            args:
            - type: positional
              value: src/main.py
            - type: positional
              value: --transport
            - type: positional
              value: http
            - type: positional
              value: --host
            - type: positional
              value: 0.0.0.0
            - type: positional
              value: --port
            - type: positional
              value: "3000"
            command: python3
          origin:
            identifier: localhost:5001/mymcp:latest
            oci:
              serverName: mymcp
            type: oci
          transport:
            path: /mcp
            port: 3000
            type: http
        repository:
          url: https://github.com/solo-io/doc-examples
          subfolder: agentregistry/mymcp
      title: mymcp
    EOF
  5. Publish the MCP server to the Solo Enterprise for agentregistry catalog.

    arctl apply -f mymcp/mcp.yaml

    Example output:

    ✓ MCPServer/mymcp (latest) created
    
  6. Verify that the MCP server is registered in the catalog.

    arctl get mcp

    Example output:

    NAME    TAG      DESCRIPTION
    mymcp   latest   mymcp MCP server
    

Step 9: Publish an agent

In this step, you use the built-in scaffolding capability to create a Python agent with Google’s Agent Development Kit (ADK) that uses AWS Bedrock Claude as the LLM provider. The agent references the MCP server you published in the previous step so that, after deployment, it can call the MCP tools during a conversation.

  1. Create an agent.

    The following command creates the myagent Python agent with the Google ADK framework configured to use the AWS Bedrock Claude provider.

    arctl init agent myagent --framework adk --language python \
      --model-provider bedrock \
      --model-name us.anthropic.claude-sonnet-4-6
    Want to see available options as you configure your agent? Try out the interactive CLI mode that walks you through the details for your agent scaffold. Simply run arctl init agent to start the CLI in interactive mode.

    Example output:

    ✓ Created agent: myagent (framework: adk, language: python, model: bedrock/us.anthropic.claude-sonnet-4-6)
    
    🚀 Next steps:
     1. Publish to the registry:
        arctl apply -f myagent/agent.yaml
    
  2. Explore the agent scaffold that was created for you. You can optionally make changes to the files to customize your agent further.

    ls myagent
    FileDescription
    arctl.yamlThe agent details that you specified with the arctl CLI.
    agent.yamlThe agent definition, including the agent framework, LLM provider, model, and agent image location.
    docker-compose.yamlA Docker compose file that is used to spin up and run your agent on your local machine.
    DockerfileThe Dockerfile to build and run your agent in a container.
    myagentA directory that includes the agent.py script that defines the agent.
    otel-collector-config.yamlAn OpenTelemetry configuration for the agent.
    pyproject.tomlThe dependency definition of your agent.
    README.mdAn introduction to the agent with instructions for how to customize it.
  3. Store the agent source code in a GitHub repository.

    This tutorial uses the https://github.com/solo-io/doc-examples Solo.io GitHub repository so that you do not have to push your agent code to a GitHub repository. Simply continue with the next step to publish the agent. If you already have an agent in a GitHub repository, you can use that repository URL instead.
  4. Edit the agent.yaml file to add the GitHub repository information and reference the MCP server. By default, the scaffold only includes the image location. The mcpServers field tells the registry to wire the mymcp MCP server into the agent when it is deployed.

    cat << EOF > myagent/agent.yaml
    apiVersion: ar.dev/v1alpha1
    kind: Agent
    metadata:
      name: myagent
    spec:
      source:
        image: localhost:5001/myagent:latest
        repository:
          url: https://github.com/solo-io/doc-examples
          subfolder: agentregistry/myagent
      modelProvider: bedrock
      modelName: us.anthropic.claude-sonnet-4-6
      description: myagent agent
      mcpServers:
        - kind: MCPServer
          name: mymcp
    EOF
  5. Publish the agent to Solo Enterprise for agentregistry.

    arctl apply -f myagent/agent.yaml

    Example output:

    → Injecting labels from arctl.yaml: arctl.dev/framework=adk, arctl.dev/language=python
    ✓ Agent/myagent (latest) created
    
  6. Open the Solo Enterprise for agentregistry catalog. Verify that you see the myagent agent.

  7. You can also use the arctl CLI to list published agents.

    arctl get agents

    Example output:

    NAME      TAG      PROVIDER   MODEL
    myagent   latest   bedrock    us.anthropic.claude-sonnet-4-6
    

Step 10: Deploy the MCP server

Deploy the source MCP server to AWS Bedrock AgentCore inside your VPC. The MCP server must be deployed and reach deployed status before you deploy the agent, because the agent’s deployment depends on a running MCP server backend to route tool calls through the gateway.

  1. Create a Deployment for the MCP server. The registry fetches the MCP server source code from the GitHub URL you provided when you published it, packages it for AWS Bedrock AgentCore, and provisions the runtime in the specified VPC.

    arctl apply -f - <<EOF
    apiVersion: ar.dev/v1alpha1
    kind: Deployment
    metadata:
      name: mymcp
    spec:
      targetRef:
        kind: MCPServer
        name: mymcp
      runtimeRef:
        kind: Runtime
        name: agentcore
      runtimeConfig:
        region: $AWS_REGION
        entryPointFile: src/main.py
        networkMode: vpc
        subnetIds:
    ${SUBNET_IDS_YAML}
        securityGroupIds:
          - ${SG_ID}
    EOF

    Example output:

    ✓ Deployment/mymcp created
    
  2. Open the Instances view in the UI and select the MCP server deployment. Monitor the Instance Logs section until the deployment status shows deployed. The initial deployment can take several minutes while Bedrock AgentCore provisions the runtime and builds the container.

Step 11: Deploy the agent

Now that the MCP server is deployed and running in your VPC, you can deploy the agent to AWS Bedrock AgentCore. The deploymentRefs field links the agent deployment to the MCP server deployment, so the registry knows to route MCP tool calls from the agent through the gateway to the MCP server.

  1. Create a Deployment for the agent. The registry fetches the agent source from the GitHub URL you provided when you published the agent, packages it for AWS Bedrock AgentCore, and provisions the runtime in the specified VPC.

    arctl apply -f - <<EOF
    apiVersion: ar.dev/v1alpha1
    kind: Deployment
    metadata:
      name: myagent
    spec:
      targetRef:
        kind: Agent
        name: myagent
      runtimeRef:
        kind: Runtime
        name: agentcore
      deploymentRefs:
        - name: mymcp
      runtimeConfig:
        region: $AWS_REGION
        workdir: agentregistry/myagent
        networkMode: vpc
        subnetIds:
    ${SUBNET_IDS_YAML}
        securityGroupIds:
          - ${SG_ID}
    EOF
    FieldDescription
    deploymentRefsLinks the agent deployment to the MCP server deployment. The registry uses this reference to wire the MCP server’s gateway backend into the agent so that MCP tool calls are automatically routed through the gateway.
    runtimeConfig.workdirThe subdirectory within the repository that contains the agent’s source code.
    runtimeConfig.networkModeSet to vpc to deploy the agent inside your private VPC. This is required for the agent to reach the MCP server over the private network.
    runtimeConfig.subnetIdsThe private subnets in which the agent runtime is deployed.
    runtimeConfig.securityGroupIdsThe security groups applied to the agent runtime. Must allow inbound traffic from the MCP server’s security group.

    Example output:

    ✓ Deployment/myagent created
    
  2. Open the Instances view in the UI and select the agent deployment. Monitor the Instance Logs section until the deployment status shows deployed. Note that the deployment can take a few minutes to complete.

Step 12: Chat with the agent

You can chat with the agent by using the Solo Enterprise for agentregistry UI or the AWS Bedrock AgentCore UI.

  1. Open the Instances view in the UI and select the myagent agent.

  2. In the Agent Details card, click Open Chat.

  3. Chat with the agent. For example, you can ask it what it can do for you. Then, hit Enter and wait for the agent to reply. Verify that the agent lists the following capabilities:

    • Roll a dice (default agent tool)
    • Check prime numbers (default agent tool)
    • Echo messages (echo MCP server tool)
    • Add numbers (sum MCP server tool)
  1. Open the AWS Bedrock AgentCore agents overview and select the agent that you deployed.

  2. Go to the ENDPOINTS card, and click the Default endpoint.

  3. Click Test endpoint to open the agent sandbox.

  4. In the agent sandbox, make sure that your agent is selected from the Runtime drop-down.

  5. In the Input card, enter the following prompt and click Run. Verify that the agent lists the following capabilities:

    • Roll a dice (default agent tool)
    • Check prime numbers (default agent tool)
    • Echo messages (echo MCP server tool)
    • Add numbers (sum MCP server tool)
    {
      "jsonrpc": "2.0",
      "id": "1",
      "method": "message/send",
      "params": {
        "message": {
          "role": "user",
          "parts": [
            {
              "kind": "text",
              "text": "what can you do?"
            }
          ],
          "messageId": "msg-1",
          "contextId": "f5bd2a40-74b6-4f7a-b649-ea3f09890003"
        }
      }
    }

Congratulations! You successfully set up Solo Enterprise for agentregistry, deployed a source-built MCP server and an AI agent to AWS Bedrock AgentCore inside your VPC, and routed all traffic through the managed agentgateway.

Next

Explore other topics.