Four protocols, one conversation.
How an API call, MCP, and the two unrelated ACPs talk to each other. Each one connects a different pair of programs and uses the words “client” and “server” in its own way. All four send their bytes over the same wire.
Before any protocol runs, a request has to reach the server.
Whenever one of these protocols crosses a network, it travels as an HTTP request, sent inside a TLS connection, which runs on top of TCP. Before the first byte reaches a server, three things happen. A name is turned into an address (DNS). A secure channel opens (TLS). Then a structured message is sent through it (HTTP). Step through one request below to see each layer added in turn.
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 an agreed-on shape for requests and responses.
An API is a contract: send a request shaped like this, and you get a response shaped like that. On the web that usually means REST over HTTP. A request has a method, a path that names a resource, headers (one of them says who's asking), and a JSON body. The server answers with a status code and a body. Every agent protocol is built on this layer. Underneath, MCP, both ACPs, and A2A are all 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. If you can read
an HTTP exchange, you can read all of them.
MCP is USB-C for models: one standard way to plug in many tools.
The Model Context Protocol is a standard way for an app to give a model tools, data, and prompts. A host (Claude Desktop, an IDE) runs one or more clients. Each client holds a 1:1 session with one server. The messages are JSON-RPC 2.0, sent over stdio (for a local server) or Streamable HTTP (for a remote one). The host keeps servers isolated: a server can't read the whole conversation or see what other servers are doing.
// 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 tool has a JSON Schema.
Resources
- what
- Context the app can read: files, DB schemas, records. Each has a URI.
- how
resources/read; the app decides what to attach.
Prompts
- what
- Reusable templates, shown to the user as slash commands.
- how
prompts/getwhen the user picks one.
POST, and the server can optionally stream replies back with SSE. It replaced the
older HTTP+SSE transport in 2025. The client can also offer features of its own to the server: sampling, roots, and elicitation.
The first ACP, Zed's Agent Client Protocol, is LSP for coding agents.
Two unrelated protocols share the initials ACP. The first is Zed's Agent Client Protocol. It sets a standard for how a code editor (the client) talks to a coding agent like Claude Code or Gemini CLI (the agent). This is the same idea as the Language Server Protocol, which let any editor work with any language. The editor starts the agent as a subprocess, and they exchange JSON-RPC 2.0 messages over stdio. The editor owns the UI and the files. The agent does the thinking, and it 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, IBM's, merged into Google's A2A.
The other ACP is IBM's Agent Communication Protocol (from IBM Research and BeeAI, March 2025). It solves a different problem:
letting agents talk to other agents. It also uses a different wire format: plain REST over HTTP. Its designers chose not to use JSON-RPC, so you can drive it with curl.
A month later, Google shipped A2A to solve the same problem. Rather than split the ecosystem, in August 2025
IBM's ACP wound down and merged into A2A under the Linux Foundation. So of the two ACPs, only Zed's is still alive
and independent. IBM's is now part of A2A's history.
A2A works like this. Each agent publishes an Agent Card at /.well-known/agent-card.json. The card is a machine-readable
business card that lists the agent's skills and the auth schemes it accepts. Agents then exchange Tasks, Messages, and
Artifacts over JSON-RPC (or gRPC, or REST). They use SSE for streaming and webhooks for push notifications.
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 pass through all four protocols.
Say you type a request to a coding agent inside your editor. That one request can pass through all four protocols, each at its own seam, and each one ends up as bytes on the wire. Step through 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 place where data from a less trusted source meets a program with more authority. Get the boundary wrong and low-privilege input gets to use a high-privilege capability. The most important boundary in any agent sits between its trusted instructions and the untrusted data it reads. Three kinds of failure keep showing up.
Confused deputy
- what
- A middleman with special access (a gateway, an MCP proxy) is tricked into using its authority for a caller who doesn't have that authority.
- where it bites
- OAuth proxies with a shared client ID and a cached consent cookie. CSRF is the web's classic case.
- the fix
- Ask for consent per client, and match
redirect_uriexactly. Never act on ambient authority, like a cookie the browser attached on its own.
Prompt injection
- what
- Untrusted text contains instructions, and the model obeys them. The root cause is the same as SQL injection: trusted and untrusted text mixed in one stream.
- where it bites
- Any tool result or fetched page that ends up in the model's 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
- It 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. The exception is mTLS, where the proof is part of the TLS
handshake itself. Here are the four schemes you'll meet, roughly from weakest and simplest to strongest and most work to set up. Pick one to 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 limits the damage
- 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 a big target
mTLS
- proves
- possession of a private key
- lifetime
- the certificate's validity
- risk
- certificates are a lot of work to manage
Every protocol here is a contract at a seam.
A protocol is an agreement about the shape of a message and what its words mean. The API, MCP, Zed's ACP, IBM's ACP, and A2A each set up a contract at a different seam. The parties at each seam take the names “client” and “server” depending on where you stand. Underneath, they all come down to the same DNS lookup, TLS handshake, and HTTP request you met at the start.
So the interesting questions are about 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.
The facts here come from the RFCs, the official protocol specs, and the foundations that now maintain them. Where a protocol changes fast (MCP, A2A), I cite the spec version I read. MCP's current revision is 2025-11-25, and IBM's ACP merged into A2A in August 2025.