ACP (Agent Client Protocol)
meka acp speaks the Agent Client Protocol over stdio so editor / web / messenger clients can drive a meka turn end to end. Where Interactive Mode and One-Shot Mode are for humans, ACP is for programs that want to host meka inside a richer UI: streamed diffs, native apply-buttons, hosted terminals, and slash-command palettes.
This page describes what meka’s ACP surface looks like to a client. Editor-specific setup belongs in each editor’s own documentation; the protocol contract is the same everywhere.
Starting an ACP server
meka acp
The process speaks JSON-RPC 2.0 with newline framing on stdio. There is no human-facing prompt; the binary is meant to be spawned by a client that owns the conversation. The client sends initialize, then session/new (or session/load / session/resume), then a series of session/prompt calls.
A few flags are worth knowing:
| Flag | Effect |
|---|---|
-v | Logs to stderr at info (incoming client identity, session lifecycle). |
-vv | debug (per-request JSON-RPC diagnostics). |
RUST_LOG=meka=trace | Trace level. |
On startup, after the client’s initialize arrives, meka logs ACP client connected: <name> <version> so you can confirm the client identity in -v mode.
What meka advertises (agentCapabilities)
These are returned in InitializeResponse.agentCapabilities:
loadSession: true: the client may callsession/loadwith any persisted session id.sessionCapabilities.list: the client may callsession/listto browse the persisted session catalogue (cwd-filtered, cursor-paginated; sub-agent audit sessions are hidden).sessionCapabilities.resume: the client may adopt a persisted session id without replaying history.sessionCapabilities.close: the client may release the active session slot.promptCapabilities.embeddedContext: true: the client may inline @-mentioned file contents as embeddedresourceblocks (see Prompt turn).promptCapabilities.image: follows the active profile’svisionflag (defaulttrue; setvision = falsein[providers.<name>]for a text-only model). Whentrue, the client may attachimageblocks.
mcpCapabilities is intentionally not advertised. meka is itself an MCP client, but the servers it consumes are configured via meka’s own config.toml rather than the mcpServers field on session/new. Advertising HTTP/SSE while silently ignoring the client’s array would have been misleading; the marker will return when client-supplied MCP server connections are actually implemented.
agentInfo carries meka’s name ("meka") and the running binary version.
What meka consumes (clientCapabilities)
The client advertises these in InitializeRequest.clientCapabilities; meka stashes them and lets the built-in tools route accordingly:
fs.readTextFile: true:read_fileissuesfs/read_text_file { sessionId, path, line?, limit? }so the client serves the in-buffer view of the file. Image and regexread_filemodes have nofs/*analogue and stay local.fs.writeTextFile: true:write_fileandedit_file’s apply step issuefs/write_text_file { sessionId, path, content }. meka still attaches diff metadata to thetool_call_updateso clients with an apply-diff UI can render it.terminal: true:execute_commandruns the four-call dance:terminal/create→terminal/wait_for_exit→terminal/output→terminal/release. Onsession/cancelor a per-call timeout, meka issuesterminal/killand still reads accumulated output. Exception: inreadpermission mode meka keeps the local sandboxed shell (Landlock / bwrap / sandbox-exec / Low-Integrity) rather than delegating, so the sandbox isn’t bypassed by the client’s terminal.
If the client omits a capability, the matching tool falls back to local syscalls; the user-visible behaviour is the same as meka in the REPL.
Session lifecycle
meka holds an in-memory map of sessionId → SessionEntry. Any number of sessions can coexist in one meka acp process, each with its own cwd, permission level, conversation, cancellation token, and per-session runtime mutex. Prompts on different sessions run in parallel; a second session/prompt for a session that already has one in flight is rejected with InvalidParams. The session row is also locked on disk (the same lock the REPL uses), so two meka processes can’t simultaneously write events for the same session id.
session/new { cwd, mcpServers }: mints a fresh persisted session, captures the cwd, takes the on-disk session lock, returns the session id and the currentSessionModestate.session/load { sessionId, cwd, mcpServers }: replays the persisted conversation as a stream ofsession/updatenotifications (user_message_chunk,agent_message_chunk,agent_thought_chunk,tool_call,tool_call_update) before the response. Orphan tool calls (the persisted log stopped mid-tool) are closed out with afailedstatus so the client’s UI doesn’t render a stuck spinner. If the client’scwddiffers from the persisted one, meka updates the persisted row to match; the client wins.session/list { cwd?, cursor? }: paginated index. Filtered to the requested cwd when present; sub-agent sessions are always hidden.nextCursoris opaque; round-trip it back to keep paging.session/resume { sessionId, cwd, mcpServers }: adopts the session id without replaying. Use this when the client already has the history rendered. Same cwd-update behaviour assession/load.session/close { sessionId }: cancels any in-flight prompt, releases the on-disk session lock, and removes the entry from the map.session/cancel { sessionId }: interrupts the activesession/prompt. The response carriesstopReason: "cancelled". If a cancel arrives between turns (after one prompt completed and before the next is installed), meka latches the signal and cancels the next prompt immediately on arrival.session/set_mode { sessionId, modeId }: flips the agent’sPermissioncell. Modes outside[permissions].enabledfrom the config become JSON-RPC errors. On success, meka emitssession/update: current_mode_update. The flip is atomic and applies to the next tool call within an in-flight turn; no need to wait for the turn to finish.
Prompt turn
A session/prompt carries a prompt array of ContentBlocks. meka accepts:
text: the baseline.resource_link: flattened into a<resource_link name="…" uri="…">description</resource_link>tag inside the prompt text so the model sees the reference; meka does not fetch the resource server-side.resource(embedded @-mention contents): a text resource is inlined as a<resource uri="…">…contents…</resource>tag; a binary (blob) resource becomes a self-closing<resource uri="…" encoding="base64"/>marker (the payload is not inlined).image: accepted only when the profile has vision on. The payload is normalized through meka’s image pipeline (size cap, format conversion) and forwarded to the model as native vision input (Claudeimage, OpenAI chatimage_url, Codexinput_image).
audio blocks (and image when vision = false) produce InvalidParams.
While the turn runs, meka streams session/update notifications:
agent_message_chunkfor each piece of assistant text.agent_thought_chunkfor thinking blocks (Claude OAuth / extended-thinking models).tool_callwhen a tool starts, withkind,status: "in_progress", an absolutelocationsarray (relative paths resolved against the session cwd, with the start line forread_file), the raw input, and a human-readabletitle. The title is the tool’s primary argument, so editors show what’s running rather than the bare tool name: the command forexecute_command,Read <path>/Edit <path>/Write <path>for file tools,Fetch <url>,Web search: <query>, etc.tool_call_updatewhen a tool finishes, with the finalstatus(completed/failed), acontentarray, andraw_output(the structured tool result).execute_commandoutput is wrapped in a fencedconsolecode block so editors render it monospaced;edit_fileandwrite_filepopulate diff content blocks so clients can render the apply-diff UI. (Large outputs offloaded to the scratchpad show the scratchpad reference rather than the full payload.)planwhenever the agent’stodotool updates the task list, so clients with a plan panel (e.g. Zed) render the live to-do list. meka’scancelledtodo status maps tocompleted.session_info_updateonce per session, carrying the title (the first user message preview) so a freshly created or loaded tab gets a label without asession/listcall.usage_updateafter each turn, carryingused(tokens currently in context: all input tiers plus output) andsize(the model’s context window), so clients with a context gauge (e.g. Zed) show how full the window is. Emitted only once both values are known.
The response carries a final stopReason:
stopReason | Meaning |
|---|---|
end_turn | The agent finished cleanly. |
max_tokens | The provider stopped because the model hit its maximum output tokens. The assistant message may be truncated. |
cancelled | session/cancel interrupted the turn, including the case where the cancel caused an exception in an underlying operation. meka probes the per-session cancellation token after run_turn; any error returned while the token has fired surfaces as cancelled rather than a generic JSON-RPC error. |
refusal | The model declined to comply (Claude stop_reason: "refusal" and the OpenAI equivalent). The assistant message contains the refusal text. |
Permission modes
meka’s Permission levels map 1:1 to ACP SessionMode ids:
| Permission | Mode id | Display name | Description |
|---|---|---|---|
None | none | None | No tools available. |
Read | read | Read | File reads and searches only. No writes, no shell. |
Ask | ask | Ask | Every write or shell command requires approval. |
Write | write | Write | All tools allowed without per-call approval. |
The full mode picker is advertised on every session-creation response (NewSessionResponse.modes, LoadSessionResponse.modes, ResumeSessionResponse.modes) but only the modes in [permissions].enabled from your config.toml are listed; picking a disabled mode would just error.
When the active mode is ask, write-gated tools trigger a session/request_permission round-trip. Clients render four options:
- Allow: run this call only.
- Always allow: run this call and skip the prompt for the same tool for the rest of the session.
- Deny: refuse this call only.
- Always deny: refuse this call and every subsequent call to the same tool.
Sticky decisions live in meka’s process memory; they reset on session close.
Slash commands
Two kinds of slash command are advertised through session/update: available_commands_update (after session/new / session/load / session/resume, and refreshed at the top of every session/prompt so a skill installed mid-session shows up without a reconnect):
- Built-in local commands —
/status(model, context usage, tokens, mode) and/mcp(configured MCP servers and their connection status). They render text back as anagent_message_chunkand end the turn immediately, with no model call. - Skills (see Skills) — each installed skill is a top-level command carrying a free-form input hint (
"additional context (optional)").
When the user picks one from the palette, the client typically inserts /<name> and lets the user type extra context. meka parses the prompt as follows:
- A built-in local command (
/status,/mcp): handled agent-side, output streamed back, turn ends with no model call. Checked first, so a skill can’t shadow a built-in (a skill namedstatus/mcpis dropped from the palette). - Plain text (no leading slash): passes through to the model unchanged.
/<skill-name>matching an installed skill: loads the skill body via the same path as the REPL’s/skillcommand (substituting${MEKA_SKILL_DIR}and${MEKA_SESSION_ID}) and prepends any extra context the user typed.- Slash with a syntactically valid but unknown skill name (
/nonexistent): JSON-RPC error. - Slash with content that isn’t a valid skill identifier (
/etc/hosts,//comment): passes through to the model unchanged, so pasted paths and code comments don’t get intercepted.
Sub-agents
spawn_agent and skill-based delegation produce a sub-agent that runs through PermissionForwardingFrontend. The sub-agent’s own output isn’t streamed to the client (its final report flows back through the parent’s tool_call_update), but its permission prompts, fs delegates, and terminal delegates all forward through the parent’s connection, so the editor’s apply-diff UI sees a sub-agent’s writes the same as the main agent’s.
Known limitations
- Tool-call diff metadata isn’t persisted. A session reopened with
session/loadreplaystool_call_updates as plain text rather than diffs. The on-disk content is unaffected. readmode +terminalcapability: meka runs the local sandboxed shell instead of delegating, to preserve the read-only jail. The shell appears in meka’s own output rather than the client’s terminal pane until you switch toaskorwrite.- Image and regex
read_file: stay local. Thefs/read_text_filerequest carries only text, so there’s no protocol surface to delegate either case. audioprompts: not supported;audiocontent blocks produceInvalidParams.- No client-side model gate for images: when
visionis on, meka forwards images to whatever model the profile names; a non-vision model returns a provider error rather than meka rejecting up front. Setvision = falsefor text-only endpoints.