Protocols: HTTP, MCP, A2A & AG-UI
One container, four wire contracts
- Explain why a Runtime declares a single protocol mode and how AWS routes the container port for each of HTTP, MCP, A2A, and AG-UI
- Compare the four protocols across port, mount path, message format, discovery mechanism, and authentication
- Host an MCP server inside Runtime using `streamable-http` on `POST /mcp`, defaulting to `stateless_http=True` and accepting the platform-generated `Mcp-Session-Id`
- Decide when stateful MCP (`stateless_http=False`) is required — elicitation and sampling — versus the stateless default
- Serve an A2A agent on port 9000 with `serve_a2a`, exposing an Agent Card at `/.well-known/agent.json`
- Identify the per-protocol discovery and auth differences (SigV4 vs OAuth 2.0, 403 vs 401 responses, session headers)
An AgentCore Runtime container declares exactly ONE protocol mode, and AWS routes the container's listening port accordingly. This lesson compares the four supported wire contracts — HTTP, MCP, A2A, and AG-UI — by port, path, format, discovery, and auth, then goes deep on the two that change how you write your server: hosting an MCP server inside Runtime (streamable-http, stateless by default, the platform-supplied `Mcp-Session-Id` you must accept) and serving Agent-to-Agent (A2A) with the SDK's `serve_a2a` helper and Agent Cards.
- 1One container, one declared protocol
- 2The four protocols side by side
- 3HTTP: the default request/response surface
- 4Hosting an MCP server inside Runtime
- 5A2A: serving an agent other agents can find
- 6AG-UI, plus the discovery and auth differences
One container, one declared protocol
AgentCore Runtime is framework- and protocol-agnostic: it doesn't care whether your agent is Strands, LangGraph, or hand-rolled, and it speaks four different wire contracts. The catch is that a single Runtime declares exactly one protocol mode, and AWS routes the container's listening port to match it. You don't run all four at once in one container — you pick the contract that fits the consumer.
Think of the protocol as the shape of the conversation at the container boundary, independent of the agent logic inside:
- HTTP — a request/response (or streaming) REST surface for apps and the AWS SDK. The default for "invoke my agent."
- MCP — your container is a Model Context Protocol server; clients call it as a tool provider over JSON-RPC.
- A2A — your container is an Agent-to-Agent peer that other agents discover and call, also JSON-RPC.
- AG-UI — an interactive event stream for rendering an agent's progress in a UI.
The protocol choice drives three concrete things: which port the container must listen on, which path it mounts the handler at, and what message format travels on the wire. Auth (SigV4 or OAuth 2.0) and the session model layer on top.
Key insight
Protocol is a container contract, not agent logic
The same reasoning agent can be exposed over any of the four protocols — you change the server wrapper, not the brains. HTTP via BedrockAgentCoreApp, MCP via FastMCP's streamable-http, A2A via serve_a2a. Pick the protocol by who is calling: an app/SDK (HTTP), an MCP client (MCP), another agent (A2A), or a live UI (AG-UI).
The four protocols side by side
Here is the full comparison. Memorize the port-to-protocol mapping — a container listening on the wrong port for its declared mode simply won't receive traffic.
| Feature | HTTP | MCP | A2A | AG-UI |
|---|---|---|---|---|
| Port | 8080 | 8000 | 9000 | 8080 |
| Mount path | /invocations, /ws | /mcp | / (root) | /invocations (SSE), /ws |
| Format | REST JSON / SSE / WebSocket | JSON-RPC | JSON-RPC 2.0 | Event streams (SSE / WebSocket) |
| Discovery | N/A | Tool listing | Agent Cards (/.well-known/agent.json) | N/A |
| Auth | SigV4, OAuth 2.0 | SigV4, OAuth 2.0 | SigV4, OAuth 2.0 | SigV4, OAuth 2.0 |
A few things to notice immediately:
- HTTP and AG-UI share port 8080. HTTP is the general-purpose request/response surface (plus optional streaming); AG-UI is the same port but oriented at interactive UI event streams.
- MCP and A2A both speak JSON-RPC, but they answer different questions. MCP exposes tools ("what can you do for me?"); A2A exposes an agent ("who are you and how do I delegate to you?").
- Discovery differs sharply. MCP clients enumerate tools via tool listing; A2A peers fetch a structured Agent Card from
/.well-known/agent.json. HTTP and AG-UI have no built-in discovery contract. - Auth is uniform across all four: every protocol accepts either AWS SigV4 (IAM-signed requests) or OAuth 2.0 (JWT bearer tokens). What differs is the failure response and the session header, covered later in this lesson.
Source for this contract: the Runtime service contract documentation (runtime-service-contract.html).
Watch out
Wrong port = silent failure
AWS routes traffic to the port that matches your declared protocol mode: HTTP/AG-UI on 8080, MCP on 8000, A2A on 9000. If your MCP server binds 8080 instead of 8000, the platform routes MCP traffic to a port nothing is listening on. Bind the port the protocol requires (FastMCP and serve_a2a default to the right ports), and bind to host 0.0.0.0, not 127.0.0.1.
HTTP: the default request/response surface
HTTP is the contract you get from BedrockAgentCoreApp and the one the AWS SDK's InvokeAgentRuntime data-plane call targets. The container runs on host 0.0.0.0, port 8080, platform ARM64.
Endpoints:
POST /invocations— the primary entrypoint. Input is JSON; the response is either JSON (Content-Type: application/json, e.g.{"response":"...","status":"success"}) or Server-Sent Events (Content-Type: text/event-stream, with lines of the formdata: {...}) when you stream.GET /ping— health check. Returns HTTP 200 with body{"status": "<Healthy|HealthyBusy>", "time_of_last_update": <unix_ts>}./ws— optional WebSocket on the same port 8080, established via an HTTP-Upgrade handshake (101); carries text and binary; the session is bound via theX-Amzn-Bedrock-AgentCore-Runtime-Session-Idheader.
You can exercise the exact cloud contract locally with curl — there is nothing AWS-specific about the wire format:
curl -X POST http://localhost:8080/invocations \
-H 'Content-Type: application/json' \
-d '{"prompt":"What is the weather?"}'Auth response semantics differ by auth mode — this is the cleanest way to tell, from a 4xx alone, how an agent is protected:
# OAuth-protected agent, missing/invalid token:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="…/.well-known/oauth-protected-resource?qualifier={QUALIFIER}"
# SigV4 (IAM)-protected agent, unauthorized caller:
HTTP/1.1 403 Forbidden
ACCESS_DENIED # note: NO WWW-Authenticate headerAn OAuth agent returns 401 + a WWW-Authenticate: Bearer … header pointing at its protected-resource metadata; a SigV4 agent returns 403 + ACCESS_DENIED with no WWW-Authenticate.
Tip
Streaming is just a Content-Type switch
The same POST /invocations endpoint serves a single JSON body or an SSE stream — the client distinguishes them by Content-Type. With BedrockAgentCoreApp, an async generator entrypoint auto-serves text/event-stream and each yield becomes one data: SSE event. No separate endpoint, no separate protocol mode.
Hosting an MCP server inside Runtime
When you declare the MCP protocol, your container becomes a Model Context Protocol server that MCP clients call as a tool provider. The contract is specific:
- Port
8000, single endpointPOST /mcp, messages are JSON-RPC. - Transport must be
streamable-http. This is non-negotiable — the older stdio transport is for local processes, not a hosted Runtime; SSE-only transport is not the contract here. - Default to stateless mode:
stateless_http=True. Stateless is the right default for a horizontally-scaled, session-isolated Runtime where any microVM can answer any request. - The platform always injects and returns an
Mcp-Session-Id. Your server must accept the platform-generated ID — it must not reject or override it. This is the single most common mistake when porting a local MCP server: a server that insists on minting its own session ID will fight the platform.
The minimal hosted MCP server, using the official mcp SDK's FastMCP:
from mcp.server.fastmcp import FastMCP
# Bind 0.0.0.0; stateless is the Runtime default.
mcp = FastMCP(host="0.0.0.0", stateless_http=True)
@mcp.tool()
def add(a: int, b: int) -> int:
return a + b
if __name__ == "__main__":
# streamable-http is REQUIRED; served at /mcp on port 8000.
mcp.run(transport="streamable-http")When do you need stateful mode (stateless_http=False)? Only when the MCP interaction is genuinely multi-turn at the protocol level — specifically the two MCP capabilities that require the server to hold a live session and call back to the client:
- Elicitation — the server pauses a tool call to prompt the client (and through it, the user) for additional input mid-flight.
- Sampling — the server asks the client's LLM to generate a completion on its behalf.
If you use neither, stay stateless. Stateful mode trades scale-out simplicity for the ability to keep a conversation pinned, so reach for it only when elicitation or sampling forces your hand.
Watch out
Accept the platform's Mcp-Session-Id — don't generate your own
AgentCore always supplies the Mcp-Session-Id header and round-trips it. Your server must accept it as-is. A FastMCP server configured to generate or validate its own session IDs against a local store will reject the platform's ID and break the session. Let the platform own session identity; you just honor it.
Note
The MCP session header is different from HTTP's
MCP sessions ride the Mcp-Session-Id header. HTTP, A2A, and AG-UI all use X-Amzn-Bedrock-AgentCore-Runtime-Session-Id instead. The header name is part of each protocol's contract — don't assume one fits all four.
A2A: serving an agent other agents can find
The Agent-to-Agent (A2A) protocol turns your Runtime into a peer that other agents discover and delegate to — the multi-agent building block. The contract:
- Port
9000, mounted at/(root), messages are JSON-RPC 2.0. - Discovery via Agent Cards served at
/.well-known/agent.json— a structured document describing the agent's identity, skills, and how to call it. This is the A2A analog of MCP's tool listing.
The SDK ships a helper, serve_a2a, in the bedrock-agentcore[a2a] extra. Install the extra, wrap your framework's executor, and call serve_a2a — it wires up the Agent Card, the JSON-RPC surface, and /ping for you:
pip install 'bedrock-agentcore[a2a]'from strands import Agent
from strands.a2a import StrandsA2AExecutor
from bedrock_agentcore.runtime import serve_a2a
agent = Agent(
model="us.anthropic.claude-sonnet-4-20250514",
system_prompt="You are a helpful assistant.",
)
# Serves /.well-known/agent.json + JSON-RPC + /ping on port 9000.
serve_a2a(StrandsA2AExecutor(agent), port=9000)That single serve_a2a call replaces the boilerplate of hand-building the Agent Card endpoint and the JSON-RPC router. Note the model ID in the example is illustrative — claude-sonnet-4-20250514 is now a legacy ID; pin a current model ID from the live model catalog rather than copying this one verbatim.
The practical pattern: deploy reusable specialist agents as A2A Runtimes, publish their Agent Cards, and let an orchestrator agent discover and call them as peers — composability without hard-coding endpoints.
Tip
MCP vs A2A: tools vs agents
Both are JSON-RPC, but the mental model differs. Expose MCP when you have tools/capabilities a client should call (/mcp, tool listing). Expose A2A when you have an autonomous agent other agents should discover and delegate whole tasks to (/, Agent Card at /.well-known/agent.json). Same agent brains, different framing of the contract.
AG-UI, plus the discovery and auth differences
AG-UI (Agent-to-User-Interface) is the protocol for interactive UI rendering. It shares port 8080 with HTTP and mounts at /invocations (SSE) plus the optional /ws WebSocket. The wire format is event streams — a sequence of UI-oriented events (tool calls, partial outputs, state updates) that a front end consumes over SSE or WebSocket to render an agent's progress live. Where plain HTTP streaming gives you raw data: chunks, AG-UI is structured for driving a UI surface.
Discovery — three different stories:
| Protocol | How a caller discovers what's available |
|---|---|
| HTTP | N/A — you publish the request/response shape out of band |
| MCP | Tool listing — the client enumerates the server's tools over JSON-RPC |
| A2A | Agent Card at /.well-known/agent.json |
| AG-UI | N/A — the UI and agent agree on the event schema out of band |
Auth — uniform options, different mechanics. Every protocol accepts SigV4 (IAM-signed requests, e.g. via InvokeAgentRuntime) or OAuth 2.0 (a JWT bearer token). The differences show up at the edges:
- Failure response: an OAuth agent answers an unauthenticated call with
401+WWW-Authenticate: Bearer …; a SigV4 agent answers an unauthorized call with403+ACCESS_DENIEDand noWWW-Authenticateheader. - Session header by protocol — the identifier that pins a caller to a microVM differs:
| Protocol | Session header |
|---|---|
| HTTP | X-Amzn-Bedrock-AgentCore-Runtime-Session-Id |
| MCP | Mcp-Session-Id |
| A2A | X-Amzn-Bedrock-AgentCore-Runtime-Session-Id |
| AG-UI | X-Amzn-Bedrock-AgentCore-Runtime-Session-Id |
MCP is the odd one out: it uses the MCP-native Mcp-Session-Id header (the one the platform generates and you must accept), while the other three share the X-Amzn-Bedrock-AgentCore-Runtime-Session-Id header.
Example
Choosing a protocol from the consumer
Building a chatbot backend an app calls via the SDK → HTTP. Publishing a tool server for MCP clients (other agents, IDEs) to call → MCP. Standing up a specialist agent for an orchestrator to delegate to → A2A. Rendering a live 'the agent is thinking / calling a tool' UI → AG-UI. The agent code can be identical; the wrapper and declared protocol change.
Try it: Host an MCP server and an A2A agent on Runtime, and read the wire contract
Goal: experience two different protocol modes from one agent and prove you understand the port/path/session rules.
Part A — Host an MCP server (port 8000, /mcp).
- Create
mcp_server.pywith a FastMCP server bound tohost="0.0.0.0",stateless_http=True, exposing one@mcp.tool()(e.g. anadd(a, b)function), andmcp.run(transport="streamable-http"). - Run it locally and confirm it serves on port 8000 at
/mcp. Inspect the response headers and confirm anMcp-Session-Idis present. Note in a comment why your server must accept this ID rather than generate its own. - Write one sentence: when would you flip
stateless_http=False? (Answer should mention elicitation and sampling.)
Part B — Serve the same agent over A2A (port 9000, root).
pip install 'bedrock-agentcore[a2a]'.- Create
a2a_server.pythat builds a StrandsAgent, wraps it inStrandsA2AExecutor, and callsserve_a2a(StrandsA2AExecutor(agent), port=9000). Pin a current model ID from the live Bedrock model catalog (do not copy a legacy ID verbatim). - Run it and fetch
GET http://localhost:9000/.well-known/agent.json. Confirm you get an Agent Card back. HitGET /pingand confirm health.
Part C — Contrast the contracts. Fill in a small table for HTTP, MCP, A2A, and AG-UI with each one's port, mount path, message format, discovery mechanism, and session header. Then answer: which protocol uses the Mcp-Session-Id header, and which three share X-Amzn-Bedrock-AgentCore-Runtime-Session-Id? For an OAuth-protected vs a SigV4-protected agent, what status code and headers come back on an unauthenticated request?
Stretch. Sketch (no deploy required) how you'd expose the same reasoning agent over HTTP for your app AND over A2A for an orchestrator — and explain why that requires two Runtimes, not one container.
Key takeaways
- 1A Runtime declares exactly ONE protocol mode and AWS routes the container port to match: HTTP on 8080 (`/invocations`, `/ws`), MCP on 8000 (`/mcp`), A2A on 9000 (`/` root), AG-UI on 8080 (`/invocations` SSE, `/ws`).
- 2HTTP is REST JSON with optional SSE or WebSocket streaming on the same port; the SDK's `InvokeAgentRuntime` targets it, and you can test the exact contract locally with `curl` against `POST /invocations`.
- 3Hosting MCP inside Runtime requires the `streamable-http` transport on `POST /mcp` (JSON-RPC), defaults to `stateless_http=True`, and your server MUST accept the platform-generated `Mcp-Session-Id` rather than mint its own.
- 4Stateful MCP (`stateless_http=False`) is only needed for elicitation (server-initiated prompts) and sampling (server-requested LLM generations); otherwise keep it stateless for scale-out.
- 5A2A serves on port 9000 with JSON-RPC 2.0 and publishes an Agent Card at `/.well-known/agent.json`; the SDK helper `serve_a2a` (extra `bedrock-agentcore[a2a]`) wires the card, JSON-RPC, and `/ping`.
- 6AG-UI streams interactive UI events (SSE/WebSocket) on port 8080 for rendering agent progress live.
- 7Auth options are uniform (SigV4 or OAuth 2.0) but mechanics differ: OAuth fails with 401 + `WWW-Authenticate`, SigV4 fails with 403 + `ACCESS_DENIED`; MCP uses the `Mcp-Session-Id` session header while the other three use `X-Amzn-Bedrock-AgentCore-Runtime-Session-Id`.
Quiz
Lock in what you learned
Check your understanding
0 / 4 answered
1.You want your AgentCore Runtime to expose tools to MCP clients AND be callable as a REST endpoint by your app's SDK at the same time, from one container. What's the correct understanding?
2.Your hosted MCP server works locally but, once deployed to Runtime, sessions break. The server is configured with `streamable-http` and `stateless_http=True`. What is the most likely cause?
3.An unauthenticated client gets `HTTP 403` with body `ACCESS_DENIED` and no `WWW-Authenticate` header from a Runtime. What does this tell you about how the agent is protected?
4.You're standing up a specialist research agent that an orchestrator agent should be able to discover and delegate tasks to. Which protocol and discovery mechanism fit, and what SDK helper serves it?
Go deeper
Hand-picked sources to keep learning
Authoritative source for the port/path/format/discovery/auth matrix across HTTP, MCP, A2A, and AG-UI. Verify the live values before relying on them.
POST /invocations, GET /ping, /ws, and the 401 vs 403 auth-response details (WWW-Authenticate header behavior).
Hosting an MCP server: streamable-http requirement, stateless default, and accepting the platform-supplied Mcp-Session-Id.
A2A on port 9000, JSON-RPC 2.0, and Agent Cards at /.well-known/agent.json.
Source of serve_a2a and the [a2a] extra. SDK versions are volatile — check the live page for the current release.
Runtime GA deep-dive with the protocol and session model in context.