Agentic AI AcademyAgentic AI Academy

Building Gateway Targets

Lambda, OpenAPI, Smithy, MCP servers, and Runtime-as-tool

Intermediate 14 minBuilder
What you'll be able to do
  • Distinguish MCP targets (aggregated, searchable) from HTTP targets (proxied, no aggregation) and pick the right type for a backend
  • Configure a Lambda target by supplying a `toolSchema` and an OpenAPI target where each operation becomes a tool named after its `operationId`
  • Front a deployed AgentCore Runtime agent as a tool with an HTTP target (agent-as-tool)
  • Create targets with boto3 `bedrock-agentcore-control` and with the AgentCore CLI (`agentcore add gateway` / `add gateway-target`)
  • Choose inline vs S3 schema delivery against the documented size limits and verify them against the live quotas page
  • Diagnose the common target gotchas: missing `operationId`, Swagger 2.0, `oneOf`/`anyOf`/`allOf`, ignored in-spec security, and SSRF blocking
At a glance

A Gateway is only useful once it has *targets* — the backends it turns into MCP tools. This lesson walks every supported target type (Lambda with a `toolSchema`, API Gateway REST stages, OpenAPI 3.0/3.1, Smithy, a remote MCP server, and the agent-as-tool HTTP target for a Runtime agent), how each one maps operations to tools, and exactly how to create them with boto3 `bedrock-agentcore-control` and the AgentCore CLI. You will also learn the schema-delivery limits (inline vs S3) and the OpenAPI gotchas — `operationId`, Swagger 2.0, composition keywords, in-spec security, and SSRF — that trip up most first builds.

  1. 1Targets are the point of a Gateway
  2. 2The MCP target types, end to end
  3. 3Creating targets with boto3 (bedrock-agentcore-control)
  4. 4Creating targets with the AgentCore CLI
  5. 5OpenAPI: operationId is the tool name
  6. 6The HTTP target: a Runtime agent as a tool
  7. 7The gotchas that bite first builds

Targets are the point of a Gateway

A Gateway by itself is just an MCP endpoint with an authorizer. The value comes from its targets — each target is a backend the Gateway connects to and exposes as one or more MCP tools. This is how AgentCore solves the M×N integration problem (M agents × N tools): you wire a backend into the Gateway once, and every agent that can speak MCP to the Gateway gets the tool.

Targets fall into two categories that behave very differently:

CategoryMembersAggregationSemantic search3LO outbound
MCP targetsLambda, API Gateway REST stage, OpenAPI 3.0/3.1, Smithy, remote MCP server, built-in templatesYes — combined into one virtual MCP server, one tools/list spanning allYes (when enabled at creation)Varies by target (OpenAPI / remote-MCP support OAuth 3LO; Lambda / API GW / Smithy do not)
HTTP targetsAgentCore Runtime agents (agent-as-tool)No — path-based proxy routing onlyNon/a (supports Caller IAM / 2LO / passthrough)

MCP targets are aggregated. Add five MCP targets and a single tools/list returns the union of all their tools — the agent sees one tool catalog, not five servers. HTTP targets are proxied, not aggregated: the Gateway routes by path and does no MCP↔backend translation, so they are not searchable and don't participate in the combined tool list.

Every tool name the agent sees is namespaced by the target name with a triple underscore: ${target_name}___${tool_name}, for example LambdaUsingSDK___get_order_tool. That prefix is the most-stepped-on detail in the whole feature — we cover it in the gotchas section.

Key insight

One Gateway, many targets, one tool catalog

Aggregation is why teams build a single governed Gateway and keep adding MCP targets to it, rather than standing up a Gateway per backend. The agent connects to one MCP endpoint and one tools/list grows as you add targets — auth, observability, and tool naming stay centralized. HTTP (Runtime) targets are the exception: they proxy, so reach for them only when you genuinely want one agent to call another agent as a tool.

The MCP target types, end to end

All six MCP target types are aggregated into the combined tools/list and are eligible for semantic search. What differs is how the backend's operations become tools — and which outbound (egress) auth modes each supports (the auth lesson has the full matrix; not every MCP target supports OAuth 3LO — Lambda, API Gateway, and Smithy targets do not).

1. AWS Lambda — you supply the toolSchema. The Gateway can't introspect a raw Lambda, so you declare each tool's name, description, and inputSchema yourself. The Gateway lists those tools; when the agent calls one, the Gateway invokes your Lambda. Your Lambda receives the prefixed tool name (${target_name}___${tool_name}) and must strip the prefix before dispatching.

2. Amazon API Gateway REST API stages (added post-launch). Point the target at an existing REST API stage and the Gateway exposes its operations as tools — handy when you've already published an API and want agents to call it without rebuilding anything.

3. OpenAPI 3.0/3.1 schema. Each operation becomes one tool, named after its operationId (then prefixed). The Gateway translates between MCP and HTTP, calling the URL in your spec's servers[].url. Swagger 2.0 is not supported — must be OpenAPI 3.0 or 3.1. (Details and gotchas below.)

4. Smithy model. Smithy is AWS's interface definition language (IDL). The Gateway generates tools from the operations declared in the model — useful for AWS-shaped services already described in Smithy.

5. Remote MCP server. Front an existing remote MCP server through your Gateway so its tools join the aggregated catalog. It supports the server's tools (required), prompts (optional), and resources (optional).

6. Built-in integration provider templates. One-click templates for Salesforce, Slack, Jira, Asana, and Zendesk — the Gateway ships the tool definitions and the integration plumbing so you don't author a schema.

The OpenAPI media-type matrix is worth pinning down because it determines whether your spec will import cleanly:

AspectSupported
HTTP methodsGET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS
Media typesapplication/json (fully supported), application/xml, multipart/form-data, application/x-www-form-urlencoded
Not supportedoneOf / anyOf / allOf, complex parameter serializers, callbacks/webhooks, links, in-spec security schemes

Tip

Pick the target type that matches what you already have

Already publish a REST API? Use an API Gateway stage or an OpenAPI target. Have raw business logic? Wrap it in a Lambda and write a toolSchema. Run an existing MCP server elsewhere? Front it as a remote MCP target so it joins the same catalog. Need agent-to-agent? That's the HTTP/Runtime target. There's almost always a target type that avoids rewriting the backend.

Creating targets with boto3 (bedrock-agentcore-control)

Targets live under a Gateway, so the control-plane client is bedrock-agentcore-control (the same client you use for create_gateway). The Gateway itself is created with protocolType="MCP" and an authorizer; targets are then attached to it.

A Gateway, for reference:

python
import boto3

client = boto3.client('bedrock-agentcore-control')
gw = client.create_gateway(
    name="my-gateway",
    roleArn="arn:aws:iam::123456789012:role/my-gateway-service-role",
    protocolType="MCP",
    authorizerType="CUSTOM_JWT",
    authorizerConfiguration={"customJWTAuthorizer": {
        "discoveryUrl": "https://cognito-idp.us-west-2.amazonaws.com/<pool>/.well-known/openid-configuration",
        "allowedClients": ["clientId"]}},
    protocolConfiguration={"mcp": {"searchType": "SEMANTIC"}})  # enables semantic search
print(gw['gatewayUrl'])

A Lambda MCP target. The Lambda target's config lives under targetConfiguration.mcp.lambda, with the function ARN and an inline toolSchema. For larger schemas you supply an S3 reference instead of inlinePayload. Outbound credentials are declared in credentialProviderConfigurations[]:

python
client.create_gateway_target(
    gatewayIdentifier=gw['gatewayId'],
    name="LambdaUsingSDK",
    targetConfiguration={
        "mcp": {
            "lambda": {
                "lambdaArn": "arn:aws:lambda:us-west-2:123456789012:function:order-tools",
                "toolSchema": {
                    "inlinePayload": [
                        {
                            "name": "get_order_tool",
                            "description": "Look up an order by its ID",
                            "inputSchema": {
                                "type": "object",
                                "properties": {"orderId": {"type": "string"}},
                                "required": ["orderId"]
                            }
                        }
                    ]
                    # ...or an S3 reference instead of inlinePayload for larger schemas
                }
            }
        }
    },
    credentialProviderConfigurations=[]
)

The agent will see this tool as LambdaUsingSDK___get_order_tool. Lambda targets use a basic GATEWAY_IAM_ROLE outbound config — no OAuth, no API key (see the auth lesson for the full matrix).

The exact targetConfiguration JSON keys for Smithy and API Gateway targets are not nailed down in our reference — treat those shapes as something to confirm against the live API reference before you ship code against them.

Note

Control plane = bedrock-agentcore-control

Don't confuse the data plane with the control plane. You manage Gateways and targets with the bedrock-agentcore-control client. The Gateway's gatewayUrl (https://{gateway-Id}.gateway.bedrock-agentcore.{Region}.amazonaws.com) is the MCP data endpoint your agent POSTs JSON-RPC to — a different surface entirely.

Creating targets with the AgentCore CLI

The AgentCore CLI wraps the same control-plane calls in a friendlier surface. You create the Gateway, add one or more targets, then deploy:

bash
# 1. Create the gateway (NONE = no inbound auth, for dev/test only)
agentcore add gateway --name TestGateway --authorizer-type NONE --runtimes MyGatewayAgent

# 2a. Lambda target — point at the function ARN and a tool-schema file
agentcore add gateway-target --name TestLambdaTarget --type lambda-function-arn \
  --lambda-arn <ARN> --tool-schema-file tools.json --gateway TestGateway

# 2b. OpenAPI target — supply the spec and the outbound auth mode
agentcore add gateway-target --name MyOpenAPITarget --type open-api-schema \
  --schema path/to/openapi.json --outbound-auth none|api-key|oauth --gateway MyGateway

# 3. Deploy
agentcore deploy

The --tool-schema-file tools.json for a Lambda target is just the toolSchema payload — the array of {name, description, inputSchema} objects you'd otherwise inline in boto3:

json
[
  {
    "name": "get_order_tool",
    "description": "Look up an order by its ID",
    "inputSchema": {
      "type": "object",
      "properties": { "orderId": { "type": "string" } },
      "required": ["orderId"]
    }
  }
]

For the OpenAPI target, --outbound-auth is where you tell the Gateway how to authenticate to the backend (none, api-key, or oauth). Crucially, this is separate from any security scheme written into the OpenAPI document — the Gateway ignores in-spec security and uses what you configure here. There are four ways to create a Gateway in total (Console, AgentCore CLI, boto3 bedrock-agentcore-control, and the Starter Toolkit GatewayClient); CLI and boto3 are the two you'll script.

Watch out

`--authorizer-type NONE` is dev/test only

NONE means no inbound authorization — anyone who can reach the endpoint can call your tools. It's fine for a local spike, but for anything real use a JWT authorizer (CUSTOM_JWT with a discoveryUrl + allowedClients) or IAM/SigV4. Inbound auth is one-per-Gateway and covered in depth in the Gateway auth lesson.

OpenAPI: operationId is the tool name

OpenAPI targets are the most common — and the most footgun-prone — so they deserve their own pass.

The core rule: each operation becomes exactly one tool, named after its operationId (then prefixed with the target name). Consider this fragment:

json
{
  "openapi": "3.1.0",
  "info": { "title": "Orders API", "version": "1.0.0" },
  "servers": [{ "url": "https://api.example.com" }],
  "paths": {
    "/orders/{orderId}": {
      "get": {
        "operationId": "getOrder",
        "summary": "Fetch a single order",
        "parameters": [
          { "name": "orderId", "in": "path", "required": true,
            "schema": { "type": "string" } }
        ],
        "responses": { "200": { "description": "OK" } }
      }
    }
  }
}

The get /orders/{orderId} operation has operationId: getOrder, so the Gateway exposes a tool named getOrder — and the agent sees it as ${target_name}___getOrder. The Gateway handles MCP↔HTTP translation: when the agent calls the tool, the Gateway issues the HTTP request against servers[].url (here https://api.example.com/orders/{orderId}) and returns the response as an MCP tool result.

The single most common OpenAPI mistake: an operation with no operationId is not exposed as a tool at all. It's silently dropped. If a tool you expected is missing from tools/list, the first thing to check is whether that operation has an operationId in the spec.

You deliver the spec either inline or via S3, governed by size limits:

DeliveryMax size (as of writing — verify the live quotas page)
Inline (in the request)~1 MB
S3 reference~10 MB

The 1 MB / 10 MB figures are quota values and can change — treat them as a starting point and confirm against the live AWS quotas page. The practical rule holds regardless of the exact numbers: small specs inline, large specs in S3.

Example

From spec to callable tool

A 12-operation OpenAPI spec, all with operationIds, attached as the target Orders, yields 12 tools: Orders___getOrder, Orders___createOrder, Orders___cancelOrder, and so on. Drop the operationId on cancelOrder and you silently get 11 — the agent simply never sees a cancel tool, with no error to tell you why.

The HTTP target: a Runtime agent as a tool

The one HTTP target type fronts a deployed AgentCore Runtime agent as a tool — the agent-as-tool pattern. A supervisor agent calls a specialist agent through the Gateway exactly as it would call any other tool, which is the building block for multi-agent systems where one agent delegates a subtask to another.

The trade-offs versus MCP targets are real and worth internalizing:

  • No aggregation. The Gateway proxies to the Runtime agent with path-based routing; it does not fold the agent into the combined MCP tools/list.
  • No semantic search. Only MCP targets are indexed for x_amz_bedrock_agentcore_search — HTTP targets are not searchable.
  • No MCP↔backend translation. It's a proxy, not a translator.

On outbound auth, the Runtime HTTP target is the only target that supports Caller IAM (via Forward Access Sessions / FAS), alongside the Gateway service role, OAuth 2LO, and token passthrough — useful when the downstream agent must act under the caller's permissions rather than the Gateway's.

Mental model:

text
Agent (MCP client)
  -> Gateway (one MCP endpoint)
       |-- MCP targets:  Lambda / OpenAPI / Smithy / API GW / remote MCP   [aggregated, searchable]
       \-- HTTP target:  AgentCore Runtime agent                           [proxied, agent-as-tool]

Key insight

Agent-as-tool vs. aggregated tools

Reach for an HTTP/Runtime target when the thing you want to call is itself an agent with its own reasoning loop — a specialist you want a supervisor to delegate to. Reach for MCP targets when you want discrete, searchable tools (a function, an API operation) in one shared catalog. Mixing both in one Gateway is fine; just remember the HTTP target won't show up in tools/list aggregation or semantic search.

The gotchas that bite first builds

Most failed target setups come down to a short list of well-known traps. Memorize these — they're the difference between a tool that works and a tool that silently never appears.

  • Forgetting operationId. An OpenAPI operation without an operationId is not exposed as a tool. No error — it just isn't there. Check this first when a tool is missing.
  • Not stripping the ${target_name}___ prefix. Your downstream Lambda (or MCP server) receives the prefixed tool name. Dispatching on the raw name without stripping ${target_name}___ means your handler doesn't recognize it. Tool names are capped at 256 characters (a quota — verify on the live page).
  • Swagger 2.0 is not supported. Only OpenAPI 3.0/3.1. Convert (or regenerate) Swagger 2.0 specs before importing.
  • Composition keywords oneOf / anyOf / allOf are unsupported, along with complex parameter serializers, callbacks/webhooks, and links. Schemas leaning on these won't import cleanly.
  • In-spec security schemes are ignored. Auth defined inside the OpenAPI document is not honored — configure outbound auth on the Gateway instead (--outbound-auth on the CLI, or credentialProviderConfigurations/outbound config in boto3).
  • SSRF protection blocks private IPs. The Gateway blocks private IP ranges and you should avoid dynamic servers[].url values. Your backend must be reachable on a public/allowed endpoint.
  • Trying to enable semantic search after creation. searchType="SEMANTIC" can only be set at Gateway creation; you can't flip it on later — you'd have to recreate the Gateway. (Strictly a Gateway-level setting, but it bites people the moment they add targets and expect search to light up.)

A quick triage table:

SymptomLikely causeFix
A tool is missing from tools/listOperation has no operationIdAdd an operationId to every operation
Lambda handler says "unknown tool"Prefix not strippedStrip ${target_name}___ before dispatch
OpenAPI import rejectedSwagger 2.0, or oneOf/anyOf/allOfUse OpenAPI 3.0/3.1; remove composition keywords
Calls to backend 401 despite spec authIn-spec security ignoredConfigure outbound auth on the Gateway
Backend unreachable / call blockedSSRF block on private IP or dynamic URLUse a public/allowed static servers[].url
Semantic search tool absentNot enabled at creationRecreate the Gateway with searchType="SEMANTIC"

Watch out

Strip the triple-underscore prefix server-side

The Gateway sends your backend the namespaced tool name, e.g. LambdaUsingSDK___get_order_tool. In your Lambda handler, split on ___ and dispatch on the suffix (get_order_tool). Forgetting this is the second-most-common Gateway bug after the missing operationId.

Try it: Wire up an OpenAPI target and a Lambda target, then trip the gotchas on purpose

Goal: attach two MCP targets to a Gateway, confirm the operationId-to-tool mapping and the triple-underscore prefix, and reproduce the two most common failures so you recognize them in the wild.

  1. Create a Gateway. Use the AgentCore CLI for speed: agentcore add gateway --name LabGateway --authorizer-type NONE --runtimes <your-agent> (NONE is fine for this local lab only). Note the returned gatewayUrl.
  2. Attach an OpenAPI target. Take any small public OpenAPI 3.1 spec (or write a two-operation one with operationIds like getOrder and createOrder and a static public servers[].url). Run agentcore add gateway-target --name Orders --type open-api-schema --schema ./openapi.json --outbound-auth none --gateway LabGateway, then agentcore deploy.
  3. List the tools. POST a JSON-RPC tools/list to {gatewayUrl}/mcp and confirm you see Orders___getOrder and Orders___createOrder — the operationIds, prefixed with the target name.
  4. Trip gotcha #1 — missing operationId. Remove the operationId from createOrder, redeploy, and re-list. Confirm Orders___createOrder silently disappears (no error). Add it back.
  5. Attach a Lambda target. Write a tiny Lambda and a tools.json with one tool ({name, description, inputSchema}). Run agentcore add gateway-target --name LabLambda --type lambda-function-arn --lambda-arn <ARN> --tool-schema-file tools.json --gateway LabGateway and redeploy.
  6. Trip gotcha #2 — the prefix. Log the incoming event in your Lambda. Call the tool through the Gateway and confirm the handler receives LabLambda___<toolname>, not the bare name. Then add a one-liner that splits on ___ and dispatches on the suffix.
  7. (Optional) boto3 equivalent. Recreate the Lambda target with boto3.client('bedrock-agentcore-control').create_gateway_target(...), placing the config under targetConfiguration.mcp.lambda.{lambdaArn, toolSchema.inlinePayload[...]}. Compare it to the CLI flags.
  8. Reflect. In three sentences: which gotcha would have cost you the most debugging time in production, and why did it produce no error message? Note where you'd verify the inline-vs-S3 schema size limits before shipping a large spec.

Key takeaways

  1. 1Targets are what make a Gateway useful: MCP targets (Lambda, API Gateway REST stage, OpenAPI 3.0/3.1, Smithy, remote MCP server, built-in Salesforce/Slack/Jira/Asana/Zendesk templates) are aggregated into one searchable `tools/list`; the HTTP target (a Runtime agent, agent-as-tool) is proxied with no aggregation or search.
  2. 2Lambda targets require you to supply a `toolSchema` of `{name, description, inputSchema}`; the Gateway invokes your function and hands it the prefixed tool name, which you must strip before dispatching.
  3. 3For OpenAPI targets, each operation becomes one tool named after its `operationId` (then prefixed `${target_name}___`); an operation with no `operationId` is silently not exposed. Swagger 2.0 is not supported — only 3.0/3.1.
  4. 4Create targets with boto3 `bedrock-agentcore-control` (`create_gateway` / `create_gateway_target`, config under `targetConfiguration.mcp.lambda`) or the AgentCore CLI (`agentcore add gateway` / `add gateway-target` / `deploy`).
  5. 5Deliver schemas inline (~1 MB) or via S3 (~10 MB) — these are quotas that can change, so verify them on the live AWS quotas page; small specs inline, large specs in S3.
  6. 6Watch the gotchas: missing `operationId`, unstripped `${target_name}___` prefix, Swagger 2.0, `oneOf`/`anyOf`/`allOf`, ignored in-spec security (configure auth on the Gateway), and SSRF blocking of private IPs / dynamic `servers[].url`.

Quiz

Lock in what you learned

Check your understanding

0 / 4 answered

1.You import an OpenAPI 3.1 spec as a Gateway target. It has eight operations, but only seven tools show up in `tools/list`. What's the most likely cause?

2.Your Lambda is wired in as a target named `LambdaUsingSDK`, but its handler logs 'unknown tool' whenever the agent calls `get_order_tool`. What's wrong?

3.Which statement about HTTP (Runtime) targets versus MCP targets is correct?

4.Your OpenAPI spec defines a Bearer-token security scheme, but the Gateway's calls to your backend return 401. The spec is valid OpenAPI 3.1. What should you do?

Go deeper

Hand-picked sources to keep learning