Skip to content

3.1 ACP Protocol Tour ​

ACP = Agent Client Protocol — a standardized stdio JSON-RPC 2.0 protocol that lets hosts ("clients") in any language drive agent runtimes ("servers"). Spearheaded by Zed Industries with a goal analogous to LSP for editors/language services: make IDEs and agents speak the same wire.

Spec: https://agentclientprotocol.com/

Quick try in 60 seconds ​

Before reading the full protocol breakdown, run a real handshake to feel the shape:

bash
pip install 'agentao[cli]>=0.5.0'
export OPENAI_API_KEY=sk-... OPENAI_BASE_URL=https://api.openai.com/v1 OPENAI_MODEL=gpt-5.4

agentao --acp --stdio

The process now reads JSON-RPC requests on stdin and writes responses + notifications on stdout. Paste these three NDJSON lines (one per line) into the same terminal:

json
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":1,"clientCapabilities":{}}}
{"jsonrpc":"2.0","id":2,"method":"session/new","params":{"cwd":"/tmp","mcpServers":[]}}
{"jsonrpc":"2.0","id":3,"method":"session/prompt","params":{"sessionId":"<id from step 2>","prompt":[{"type":"text","text":"hello"}]}}

You'll see, in order:

  • initialize response — Agentao announces capabilities (loadSession, mcpCapabilities, …)
  • session/new response — a fresh sessionId
  • session/update notifications — streaming text, tool starts, thinking
  • a final response with stopReason when the turn ends

Real host (Node skeleton) ​

javascript
import { spawn } from 'node:child_process';

const proc = spawn('agentao', ['--acp', '--stdio']);
let nextId = 1;

function send(method, params) {
  const msg = { jsonrpc: '2.0', id: nextId++, method, params };
  proc.stdin.write(JSON.stringify(msg) + '\n');
}

proc.stdout.on('data', (buf) => {
  for (const line of buf.toString().split('\n').filter(Boolean)) {
    const msg = JSON.parse(line);
    if (msg.method === 'session/update') {
      handleUpdate(msg.params);              // streamed text, tool events, thinking
    } else if (msg.method === 'session/request_permission') {
      showPermissionDialog(msg.params);       // your approval UI
    } else if (msg.id) {
      resolvePending(msg.id, msg);            // response
    }
  }
});

send('initialize', { protocolVersion: 1, clientCapabilities: {} });
// next: send('session/new', { cwd: '/your/project' })
// then: send('session/prompt', { sessionId, prompt: [{type:'text', text:'hello'}] })

Four things that catch first-timers

  • Framing is NDJSON (newline-delimited JSON) — not WebSocket, not raw stdout
  • Handshake order is mandatory: initialize → session/new → session/prompt
  • session/update is a notification (no id) — do not respond to it
  • session/request_permission is a request (has id) — the host must reply in reasonable time

The rest of this section explains why each of those four exists.

How ACP relates to MCP ​

ACP and MCP are complementary, not competitors:

ProtocolDirectionTypical clientTypical serverRole
ACPHost ↔ AgentIDE, web UI, CLIAgent runtime (e.g. Agentao)Expose the agent to UI
MCPAgent ↔ ToolsAgent runtimeTools / data sources (filesystem, GitHub, databases…)Expose tools to the agent
┌────────────┐   ACP    ┌────────────┐   MCP    ┌─────────────┐
│   Client    │◄────────►│   Agent     │◄────────►│ MCP Tools  │
│ (your host) │           │  (Agentao)  │           │ (resources)│
└────────────┘           └────────────┘           └─────────────┘

Agentao is simultaneously an ACP server (driven by hosts) and an MCP client (drives external tools).

Why ACP ​

NeedHow ACP addresses it
Non-Python hoststdio + JSON — any language can integrate
Process isolationAgent runs in a subprocess; a crash does not kill the host
Swappable agent backendAgentao, Claude Code, Zed's built-in agent all speak the same protocol
AuditableOn-the-wire JSON is naturally dumpable / replayable / diffable

Protocol characteristics ​

  • Transport: stdin/stdout (v1 only)
  • Framing: NDJSON — one complete JSON object per line, delimited by \n
  • RPC: JSON-RPC 2.0
  • Connection model: one client ↔ one server, long-lived
  • Version: integer ACP_PROTOCOL_VERSION = 1 (strictly typed, not a date string)
  • Capability negotiation: both sides advertise supported features during initialize

The four message quadrants ​

         Request (has id)              Notification (no id)
        ──────────────────────────── ────────────────────────────
Client  initialize, session/new,      (not used in v1)
 →      session/prompt, session/cancel,
Server  session/load
        ──────────────────────────── ────────────────────────────
Server  session/request_permission,   session/update
 →      _agentao.cn/ask_user            (streaming text, tool events, thinking…)
Client

Key points:

  • Client → Server is the active driver (start session, send prompt, cancel)
  • Server → Client sends both notifications (continuous streaming updates) and requests (asks the user to approve a tool)
  • Both directions are multiplexed over the same stdio pair; JSON-RPC id separates requests from responses

A full round-trip ​

Reading the diagram

  • Solid arrow (→) = JSON-RPC request (has id, must be answered)
  • Dashed arrow (-->) = JSON-RPC response to a prior request
  • --) = JSON-RPC notification (no id, no reply allowed)

Extension: _agentao.cn/ask_user ​

The base spec only lets the server request permission — it does not let the server ask the user a free-form question. Agentao advertises a private extension method _agentao.cn/ask_user under _meta["_agentao.cn/extensions"] (ACP's standard extension channel) to do exactly that. Client options:

  • Implement it: prompt the user, reply {"outcome":"answered","text":"..."} (see C.11)
  • Don't implement: the error response resolves to the sentinel "(user unavailable)", and the turn continues

ACP v1 boundaries ​

Explicit v1 limits (Agentao's capability block reflects them faithfully):

  • promptCapabilities.image = true (0.4.8+, inline {data, mimeType} blocks, no by-reference uri; the optional annotations / _meta are ignored), audio = false, embeddedContext = false
  • mcpCapabilities.http = true, sse = true — MCP transports are stdio, Streamable HTTP, and SSE
  • authMethods — one terminal login method (--login) when the client declares Terminal Auth (clientCapabilities.auth.terminal, or the legacy _meta["terminal-auth"]), otherwise []. Credentials never cross the wire: they come from the launch environment, the project .env, or ~/.agentao/llm.json written by agentao --login, and a session with none gets auth_required (-32000) from session/new / session/load (0.5.8+)

Future versions will expand these. Clients should inspect the handshake response before deciding what prompt format to send.

What's next ​

  • 3.2 Step-by-step Agentao-as-server with complete wire traces
  • 3.3 Build a host ACP client skeleton
  • 3.4 Reverse: Agentao calling other ACP servers
  • 3.5 Zed integration

→ 3.2 Agentao as an ACP Server