Building an ACP Agent from Scratch Using Rust

The Situation with ACP and Rust

When I started exploring the Agent Client Protocol for a project where I wanted to build a coding assistant that worked natively in Zed and other ACP-compatible editors, I quickly noticed that the ecosystem is TypeScript/Node.js-first. The official ACP reference implementationsarrow-up-right β€” from Codex CLI, Gemini CLI, Cline, and others β€” are mostly Node.js or Python.

As of early 2026, there is no official Rust SDK for ACP. This article is about implementing it from the protocol specification directly.

The good news: ACP is a clean JSON-RPC 2.0 protocol over stdio. If you've read the spec, the wire format is straightforward. And Rust has everything we need to implement it properly: async I/O, excellent JSON support, and precise control over stdio framing.

This article walks through building a minimal but complete ACP-compliant agent in Rust β€” one that can negotiate capabilities with an ACP client (like Zed), create sessions, receive prompt turns, and stream back responses.


ACP Wire Protocol Recap

Before writing code, it's worth being precise about the protocol. Everything is newline-delimited JSON-RPC 2.0 over stdin/stdout.

Methods the Agent must implement (baseline)

Method
Direction
Description

initialize

Client β†’ Agent

Version negotiation and capability exchange

session/new

Client β†’ Agent

Create a new conversation session

session/prompt

Client β†’ Agent

Send a user message; agent processes and responds

Notifications the Agent sends

Notification
Direction
Description

session/update

Agent β†’ Client

Streaming output: text chunks, tool calls, diffs, thoughts

Methods the Agent can call on the Client

Method
Direction
Description

session/request_permission

Agent β†’ Client

Ask user to approve a tool call

The key distinction from MCP: ACP is bidirectional. The agent makes requests of the editor (for file system access, terminal, permission gating), not just the other way around.


What We're Building

A minimal but spec-compliant ACP agent that:

  1. Handles initialize β€” capability negotiation

  2. Handles session/new β€” creates a session with a unique ID

  3. Handles session/prompt β€” receives a user message, sends back streaming session/update notifications, and returns a final response

  4. Runs over stdio

The actual AI logic is a placeholder β€” the focus here is the correct ACP framing and protocol flow.


Project Setup

No ACP SDK needed β€” we implement the protocol directly from the spec.


Understanding the Message Framing

ACP uses JSON-RPC 2.0 over stdio with newline delimiters. Each message is a single JSON object followed by \n.

Incoming (from editor/client):

Outgoing (from agent/us):

  • Responses (reply to a request, must include the same id):

  • Notifications (no id field, no response expected):


Implementation

Step 1: Define the JSON-RPC Message Types


Step 2: Define ACP-Specific Types


Step 3: The Agent State


Step 4: The Main Dispatch Loop

This is the core of the ACP agent: read a line from stdin, parse it as JSON-RPC, dispatch to the right handler, write the response(s) to stdout.


Building


Testing the Protocol Manually

Since ACP is newline-delimited JSON-RPC over stdio, you can test the protocol sequence directly with a shell pipeline. The messages must be sent sequentially as ACP is stateful.

Write a test script:

The output will be (simplified):


Registering with Zed

To use your agent in Zed, add it to Zed's agent configuration (check Zed's ACP documentationarrow-up-right for the exact format, as it evolves with new releases):


What to Add Next

This is a skeleton. A production ACP agent built on this foundation would add:

Actual LLM Integration

Replace the placeholder response in handle_session_prompt with a call to an LLM API (e.g., OpenAI, Anthropic, a local Ollama instance). Stream tokens as they arrive using session/update notifications.

MCP Client Integration

The session/new params include a list of MCP servers for the agent to connect to. Use rmcp (the Rust MCP SDK) to connect to those servers and make tools available to your LLM:

File System Access (Client Methods)

If the client declares fs.readTextFile: true during initialization, you can call back to the client to read files:

Permission Gating

Before executing destructive tool calls, use session/request_permission:


Key Rules to Remember

  1. All logging goes to stderr. stdout is the protocol wire. println! is off-limits. Use tracing with .with_writer(std::io::stderr).

  2. Notifications have no id field. If you accidentally include one, the client may treat it as a response to a request.

  3. session/prompt must not return until all session/update notifications are sent. The client considers the turn complete when it receives the session/prompt response.

  4. Protocol version is an integer (1), not a date string. ACP uses "protocolVersion": 1, unlike MCP which uses "protocolVersion": "2025-06-18".

  5. File paths must be absolute. The spec requires all path arguments to be absolute paths.


Protocol Spec Reference

Last updated