Reference: Agent Client Protocol surface
Neutral description of what
mm -acpexposes over JSON-RPC 2.0 on stdio, throughgithub.com/coder/acp-go-sdkv0.13.5.
Transport
| Aspect | Value |
|---|---|
| Input | stdin, one JSON-RPC message per line. |
| Output | stdout, JSON-RPC messages only. |
| Logs | stderr: banner, warnings, [acp] … lines, SDK logger, the failure lines of hooks (🪝 <event> hook …). |
| Lifetime | Until the client closes stdin or the process context is cancelled. |
initialize response
| Field | Value |
|---|---|
protocolVersion |
1 |
agentCapabilities |
loadSession: true when sessions.enabled is set, absent (false) otherwise. Everything else absent (false): no image or audio prompts, no HTTP MCP. |
agentInfo.name |
acp.name of the configuration, bob by default. |
agentInfo.title |
acp.title of the configuration, Bob (bash-first agent) by default; absent when empty. |
agentInfo.version |
The binary's version, the same string as mm --version (for example v0.1.7). |
authMethods |
[] |
Methods
| Method | Behaviour |
|---|---|
initialize |
Reads clientCapabilities.elicitation.form (present and non-null = form support), logs the client name, protocol version and elicitation(form)=true|false on stderr; returns the values above. |
authenticate |
Returns an empty response. |
session/new |
Creates a session with a time-sortable id (20260916-101530-4f2a9c1e) and the request's cwd, a fresh history holding the system prompt, and an empty allow-always set. mcpServers are counted in the log and ignored. Once the response is written, sends an available_commands_update notification listing the slash commands (see below). With sessions.enabled, writes <cwd>/<sessions.dir>/<id>.json at once and rewrites it after every turn, /new and /compact. |
session/prompt |
Runs one generation on the session's history, or executes a slash command without consulting the model. Turns are serialised process-wide. |
session/cancel |
Cancels the turn in flight for that session. |
session/load |
With sessions.enabled: reads <cwd>/<sessions.dir>/<sessionId>.json, replaces its system prompt by the current one, registers the session (empty allow-always set), sends the replay (below) then an available_commands_update, and answers {}. Unknown id or unreadable file: invalid params carrying the sessionId and the error. Without sessions.enabled: method not found. |
session/set_mode, session/set_config_option, session/list, session/resume, session/close, logout |
method not found. |
Skills as commands
ACP has no notion of a skill. A prompt carries text, image, audio, resource or resource_link — the last being what an editor's @ file picker sends — and there is nothing for "run this procedure".
AvailableCommand is the slot, and the protocol's own examples say so: its documentation offers create_plan and research_codebase, which are procedures rather than session commands. So each skill of the catalogue is advertised there, and the editor lists it in its own / menu:
/compact Compress the history now, keeping the last turns
/new Clear the history and start a new session
/bg-jobs [input: parameters, if …] Start, wait on, inspect and stop a long-running background job…
/greetings [input: parameters, if …] Greet somebody with a festive message.
/vulcan-salute [input: parameters, if …] Send a vulcan salute to somebody.
The input.hint is what carries the parameters: the protocol says "all text that was typed after the command name is provided as input", which is exactly what a skill takes.
| Aspect | Behaviour |
|---|---|
What happens on /greetings Bob |
Not intercepted. It is rewritten as the $greetings Bob notation and the turn runs normally, so the model still makes the read_skill call — the loop detector, the permission gate and the 📖 N skill(s) recap all still see it, and mm never loads a skill behind the model's back. |
| A real command wins | session/prompt tries the interception first, so a skill called compact or new can never shadow one. Such a skill is not advertised either, and the start-up banner says so — otherwise it would be silently unreachable from the menu. |
An unknown /name |
Left exactly as typed and handed to the model, as any arbitrary slash text was before. |
Typing $greetings Bob by hand |
Works too, in the editor as in the terminal: the notation is resolved in session/prompt whether the menu wrote it or the user did. |
Note the invariant changed with this: it used to be "only advertise what session/prompt intercepts", and it is now "only advertise what this front end handles" — rewriting is handling.
session/load replay
Sent before the response, one notification per element of the stored history, in order. The system prompt is not sent.
| Stored element | Notification |
|---|---|
| User message | user_message_chunk with its text. |
Compression summary (user-role message marked by /compact) |
agent_message_chunk with 🗜️ followed by the summary. |
| Model text | agent_message_chunk. |
| Tool call with its recorded response | tool_call with toolCallId = the call's ref, title and kind as the live tool would give (bash: <command> execute, read_file: <path> read, write_file/edit_file edit, read_skill: <name> read), status: completed, rawInput, rawOutput.output and a text content holding the output. |
| Tool call without a recorded response | tool_call with status: failed and no content. |
No tool runs during a replay. Logged as [acp] session <id> loaded from <dir>: N message(s) replayed.
session/prompt
| Aspect | Behaviour |
|---|---|
Unknown sessionId |
invalid params error carrying the sessionId. |
| Prompt content | text blocks concatenated; a resource_link becomes \n[attached file: <path without file://>]; other block types are ignored. Then every @path in the text that names an existing file or directory under the session's cwd (or an absolute / ~ path) appends \n[attached file: <absolute path>] or \n[attached directory: …], once per path, and is logged as [acp] session <id>: attached <path>. |
| Slash command | When the flattened text, trimmed, is exactly /new or /compact, no generation runs; the command answers with one agent_message_chunk and stopReason: end_turn. |
/new |
The session's history is reset to the system prompt alone, the engine's last input-token count is forgotten, the chunk is 🆕 New session: N message(s) forgotten.. The session id, cwd and allow-always grants are kept. With sessions.enabled, the conversation so far is saved under a new id first (logged previous conversation kept as <id>; nothing saved when it was empty) and the session's own file is rewritten with the fresh history. Logged as [acp] session <id>: new session, N message(s) forgotten. |
/compact |
The history is compressed with the context settings, forced (the threshold is ignored, keepLastTurns is honoured). The chunk is 🗜️ compressed N messages → 1 summary + N kept (… tokens, …s), or 🗜️ nothing to compact: N message(s), no turn older than the last K (history unchanged), or [compact: failed, history kept: <explanation>] (history unchanged). The token count is forgotten only when the history changed. Waits for a running turn: turns are serialised. Logged as [acp] session <id>: /compact — <line>. |
| History | The full history returned by the engine is kept, even after a failure or a cancellation; a question with no answer at all is removed. |
| Success | stopReason: end_turn, plus usage (the session's totals: inputTokens, outputTokens, totalTokens, and thoughtTokens / cachedReadTokens when the server reported them) once the server has reported any count; absent otherwise. Also on /new and /compact. |
| Cancelled | stopReason: cancelled, with usage as above. |
| Failure | internal error with error: <one-line explanation>. |
| Log line | [acp] turn done: N command(s) · N file op(s) · N skill(s) on stderr. |
session/update notifications sent
| Update | When |
|---|---|
available_commands_update |
Once per session, right after the session/new response has been written (the editor must know the session id first, or it drops the update). Lists what this front end handles: the slash commands session/prompt intercepts, then one command per skill. Also sent at the end of a session/load replay. |
agent_message_chunk (text) |
Each streamed chunk of the model's answer, whitespace preserved; also the one-line confirmation of a slash command. |
tool_call status pending |
Before a tool runs; carries title, kind, rawInput, and locations with the absolute path when a file is involved. |
tool_call_update status in_progress |
Permission granted, tool executing. |
tool_call_update status completed or failed |
Tool finished; content is a diff for file changes, else a text block with the output; rawOutput.output always carries the output. |
tool_call then tool_call_update status failed, title <tool> — denied by a hook |
A preToolCall hook denied the call: the tool did not run and no permission was requested; the text content is [denied by a hook: <reason>]. |
usage_update (UNSTABLE in ACP) |
After every model call that reported usage (used = its input + output tokens), and at the end of every turn, /new and /compact (used = the larger of the last measure and the history's estimate). size is the context window the agent knows (contextWindow or the probe), 0 when unknown. No cost: the servers are local. |
Available commands
name |
description |
input |
|---|---|---|
new |
Clear the history and start a new session |
none |
compact |
Compress the history now, keeping the last turns |
none |
| one per skill | the skill's description |
{ "hint": "parameters, if this skill needs any" } |
Clients display the name with a leading slash and send it back as the text of a session/prompt. The two commands above are intercepted there; a skill's command is rewritten into the $skill notation and the turn runs — see Skills as commands.
session/request_permission requests sent
Sent only when yolo: false (default true: none is sent for the built-in tools), before bash, write_file, edit_file, fetch_content with output, lsp rename and the plan actions that change a plan run; never for read_skill, read_file, fetch_content without output or the lsp navigation actions. MCP tools send one when yolo: false or when their permissions is ask.
| Option id | Kind | Effect |
|---|---|---|
allow |
allow_once |
Runs the tool. |
allow_always |
allow_always |
Runs the tool and skips the dialog for that tool name for the rest of the session. |
reject |
reject_once |
Does not run; the model receives [command rejected by the user] (or [<tool> rejected by the user]). |
never |
reject_always |
Does not run, and every later call of that tool name in the session is rejected without a dialog. |
The remembered answers (allow_always, never) survive /new.
A transport error or a cancelled outcome counts as a rejection.
elicitation/create requests sent
Sent by the ask_user tool (see tools) when the client advertised clientCapabilities.elicitation.form at initialize. One request per tool call, in form mode.
| Param | Value |
|---|---|
sessionId |
The session. |
toolCallId |
The ask_user tool call, announced before by a tool_call notification. |
mode |
form |
message |
The question when there is one, else The agent has N questions for you. |
requestedSchema |
{"type": "object", "properties": {…}, "required": [every name]}, one property per question, named q1, q2…: choice → {"type": "string", "title": <question>, "enum": [options]}; multi → {"type": "array", "title": <question>, "items": {"type": "string", "enum": [options]}}; text → {"type": "string", "title": <question>}; confirm → {"type": "boolean", "title": <question>}; plus default when the question has one (a boolean for confirm). |
_meta["mm.elicitationScope"] |
{"sessionId", "toolCallId"}: a copy of the scope (acp-go-sdk v0.13.5 cannot carry the top-level fields; mm writes them from it, and an mm client relaying the question reads the tool call from it). |
| Response | Result for the model |
|---|---|
{"action": "accept", "content": {…}} |
The answers, read from content by property name (a string, a list of strings, or a boolean). |
{"action": "decline"} |
Declined. |
{"action": "cancel"}, or the turn cancelled |
Cancelled. |
| An error response | Nobody can be asked. |
Without form support, a form whose questions are all choice or confirm goes as one session/request_permission per question: toolCall.title ? <question>, toolCall.kind other, options 0, 1… named after the choices (kind allow_once; Yes/No for a confirm) and decline named Decline (kind reject_once). A cancelled outcome cancels the form; decline declines it.
When mm is the client — an agent asking another one through /ask, ask_agent or mm --ask — its initialize request advertises clientCapabilities.elicitation.form, and the asked agent's elicitation/create requests are put to the asking agent's user through its own front end. A front end that cannot ask (mm --ask) answers with an error.
Tool kinds
mm kind |
ACP ToolKind |
Tools |
|---|---|---|
execute |
execute |
bash (and any unknown kind) |
read |
read |
read_skill, read_file |
edit |
edit |
write_file, edit_file |
fetch |
fetch |
fetch_content |
other |
other |
ask_agent, ask_user |
Tool call identifiers
call_1, call_2, … numbered per process and never reset.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 |
|