Four protocols, one conversation.
How an API call, MCP, and the two rival ACPs actually talk. Different seams, different words for “client” and “server”, all riding the same wire.
Before any protocol, a packet has to find you.
Every protocol on this page is, underneath, one HTTP request riding TLS riding TCP. Before the first byte reaches a server, three things happen: a name becomes an address (DNS), a secure channel opens (TLS), and a structured message crosses it (HTTP). Step through one request and watch the layers stack up.
https:// request from a typed URL to a rendered response.# one https GET, peeled open layer by layer ip = dns_resolve("api.example.com") # name → IP, cached by TTL sock = tcp_connect(ip, 443) # SYN · SYN-ACK · ACK tls = tls_handshake(sock, sni="api.example.com") # 1 round trip; verify cert vs a CA tls.send("GET /v1 HTTP/2 · Host: ... · Authorization: ...") resp = tls.recv() # 200 OK + headers + body
An API is just an agreed-on shape for the bytes.
An API is a contract: send a request shaped like this, get a response shaped like that. On the web that usually means REST over HTTP, a method, a path that names a resource, headers (including who's asking), and a JSON body. The server answers with a status code and a body. This is the layer every agent protocol is built on. MCP, both ACPs, and A2A are all, at bottom, structured API calls.
200 ok, 201 created). 3xx look elsewhere (301 moved).
4xx your fault: 401 means not authenticated,
403 means authenticated but not allowed,
429 means you're rate limited.
5xx the server's fault (500).
GET /users you'll see tools/call or session/prompt. Learn to read
an HTTP exchange and you can read all of them.
MCP is USB-C for models: one port, many tools.
The Model Context Protocol standardizes how an app gives a model tools, data, and prompts. A host (Claude Desktop, an IDE) runs one or more clients, each holding a 1:1 session with a server. Messages are JSON-RPC 2.0, over stdio (local) or Streamable HTTP (remote). And a server can never read the whole conversation or peek at other servers, the host keeps them isolated.
// the model decided to call a tool → the client sends: {"jsonrpc":"2.0", "id":2, "method":"tools/call", "params":{"name":"get_weather", "arguments":{"city":"NYC"}}} // the server runs it and replies with the same id: {"jsonrpc":"2.0", "id":2, "result":{"content":[{"type":"text", "text":"72°F, clear"}]}}
A server exposes three primitives, split by who decides to use them:
Tools
- what
- Functions the model can invoke: query a DB, call an API, run code.
- how
tools/list, thentools/call. Each carries a JSON Schema.
Resources
- what
- Readable context: files, DB schemas, records. Each has a URI.
- how
resources/read; the app decides what to attach.
Prompts
- what
- Reusable templates, surfaced as slash commands.
- how
prompts/getwhen the user picks one.
POST for messages plus optional SSE streaming. It replaced the
older HTTP+SSE transport in 2025. The client also has its own tricks: sampling, roots, and elicitation.
The first ACP: LSP, but for coding agents.
Two unrelated protocols share the initials ACP. The first is Zed's Agent Client Protocol. It standardizes the seam between a code editor (the client) and a coding agent like Claude Code or Gemini CLI (the agent), the same way the Language Server Protocol let any editor talk to any language. The agent runs as a subprocess; they speak JSON-RPC 2.0 over stdio. The editor owns the UI and the files; the agent does the thinking and asks permission before it acts.
editor → agent initialize # negotiate versions + auth methods editor → agent session/new # a fresh conversation, its own history editor → agent session/prompt # "fix the failing test" agent → editor session/update # stream tokens + tool calls back to the UI agent → editor session/request_permission # may I run this command? agent → editor fs/read_text_file # the editor owns the files; the agent asks
The second ACP lost its own name.
The other ACP is IBM's Agent Communication Protocol (from IBM Research and BeeAI, March 2025). Different problem:
agent-to-agent. Different wire: plain REST over HTTP, deliberately not JSON-RPC, so you can drive it with curl.
Then the twist. A month later Google shipped A2A, solving the same problem. Rather than split the ecosystem, in August 2025
IBM's ACP wound down and merged into A2A under the Linux Foundation. Of the two ACPs, one is alive and independent
(Zed's). The other is now a chapter in A2A's history.
A2A works like this: each agent publishes an Agent Card at /.well-known/agent-card.json, a machine-readable
business card listing its skills and accepted auth schemes. Peers then exchange Tasks, Messages, and
Artifacts over JSON-RPC (or gRPC, or REST), with SSE for streaming and webhooks for push.
ACP · Zed
- agent client protocol
- editor ↔ agent
- wire
- JSON-RPC over
stdio - status
- Active, its own thing.
ACP · IBM
- agent communication protocol
- agent ↔ agent
- wire
- REST over HTTP
- status
- Merged into A2A, Aug 2025.
A2A · Google
- agent2agent
- agent ↔ agent
- wire
- JSON-RPC / gRPC / REST
- status
- Active, Linux Foundation.
One prompt can cross every seam.
Put it together. You type a request to a coding agent inside your editor. That single intent can ripple through all four protocols, each at its own seam, each ultimately bytes on the wire. Step the trace and watch the words client and server change meaning at every boundary.
Every seam is also a place to get fooled.
Each protocol boundary is a trust boundary: a line where data of one trust level meets authority of another. Get the boundary wrong and a low-privilege input borrows a high-privilege capability. The most consequential boundary in any agent is between its trusted instructions and the untrusted data it reads. Three failure modes recur.
Confused deputy
- what
- A privileged middleman (a gateway, an MCP proxy) tricked into using its authority for a caller who lacks it.
- where it bites
- OAuth proxies with a shared client ID and a cached consent cookie. CSRF is the web's classic case.
- the fix
- Per-client consent; exact
redirect_urimatching. Never act on ambient authority.
Prompt injection
- what
- Untrusted text carries instructions; the model obeys them. Same root cause as SQL injection: trusted and untrusted mixed in one stream.
- where it bites
- Any tool result or fetched page that reaches the context. The lethal trifecta is the worst case.
- the fix
- Cut one leg of the trifecta. Treat tool metadata as untrusted unless the server is trusted.
Token passthrough
- what
- A server forwards a token it received straight to a downstream API, instead of getting its own.
- where it bites
- Breaks rate-limits, audit trails, and trust boundaries; a stolen token turns the server into a proxy.
- the fix
- Audience-bind every token. MCP's spec forbids passthrough outright.
# the lethal trifecta: risky only when all three are true at once danger = has_private_data and reads_untrusted_input and can_exfiltrate # there is no 95%-reliable filter. the only sturdy defense is structural: assert not danger # remove a capability until one leg is gone (least privilege)
Four ways to prove you're allowed.
Every request so far carried proof of identity in the same place: the Authorization header, or, for mTLS, the TLS
handshake itself. Here are the four schemes you'll meet, in rough order of strength and ceremony. Pick one and see how it travels.
verifier = random() # the client's one-time secret challenge = sha256(verifier) # sent up front, so the code can't be stolen mid-flight code = authorize(challenge, scope, redirect) # the user consents in a browser token = exchange(code, verifier) # proves it's the same client → access token (+ refresh) call(api, auth=f"Bearer {token}") # token is audience-scoped to this one api
At a glance, weakest secret to strongest identity:
API key
- proves
- which app is calling
- lifetime
- long; rotate to revoke
- risk
- copyable; leak = full access
Bearer · JWT
- proves
- signed claims (
aud,exp) - lifetime
- short; expiry is the leash
- risk
- still bearer; needs TLS
OAuth 2.1
- proves
- the user said yes, to this scope
- lifetime
- short token, refreshable
- risk
- more parts; auth server is the prize
mTLS
- proves
- possession of a private key
- lifetime
- the certificate's validity
- risk
- heavy cert lifecycle
It's contracts all the way down.
A protocol is just an agreement about the shape of a message and the meaning of the words around it. API, MCP, the two ACPs, A2A: each draws a contract at a different seam, between parties that trade the names “client” and “server” depending on where you stand. Underneath, they all dissolve into the same DNS lookup, the same TLS handshake, the same HTTP request you met at the start.
So the interesting questions were never on the wire. They're at the seams. Who is allowed to ask. Which token proves it. What happens when an untrusted message crosses a trusted boundary. Get those right and the bytes take care of themselves.
Facts here come from the RFCs, the official protocol specs, and the foundations now stewarding them. Where a protocol moves fast (MCP, A2A), I cite the spec version I read; MCP's current revision is 2025-11-25, and IBM's ACP folded into A2A in August 2025.