ExMCP.ACP.Adapter behaviour (ex_mcp v1.0.0-rc.0)
View SourceBehaviour for adapting non-native CLI agents to ACP.
Agents like Claude Code and Codex CLI have their own protocols (NDJSON streams, one-shot JSON output). Adapters translate between ACP JSON-RPC messages and the agent's native format.
Required Callbacks
init/1— initialize adapter statecommand/1— return the executable and args to launch the agenttranslate_outbound/2— convert ACP message to native CLI formattranslate_inbound/2— convert native CLI output line to ACP messages
Optional Callbacks
capabilities/0— return static agent capabilitiespost_connect/1— called after Port is openedhandle_adapter_message/2— handle process messages for adapter-managed subprocessesshutdown/1— clean up adapter-managed resources when the bridge closesenv/1— return child-process environment variablesmodes/0— return supported operational modes for session responsesconfig_options/0— return supported config options for session responseslist_sessions/2— return available sessions (forsession/list)fork_session/2— fork an existing session (forsession/fork)auth_methods/1— return initializeauthMethodsfor adapter options
Summary
Callbacks
Return authentication methods advertised in the initialize response.
Return static agent capabilities for the initialize response.
Return the command and arguments to launch the agent subprocess.
Return the config options this agent supports.
Return environment variables for the child process.
Fork an existing session.
Handle raw messages for adapter-managed subprocesses.
Initialize adapter state from options.
List available sessions for this agent.
Return the operational modes this agent supports.
Called after the Port is opened, before any ACP messages are processed.
Clean up adapter-managed resources before the bridge exits.
Translate one line of native CLI output to zero or more ACP messages.
Translate an outbound ACP JSON-RPC message to the native CLI format.
Types
@type state() :: term()
Callbacks
Return authentication methods advertised in the initialize response.
Optional — defaults to an empty list.
@callback capabilities() :: map()
Return static agent capabilities for the initialize response.
Optional — defaults to an empty map.
@callback command(opts :: keyword()) :: {executable :: String.t(), args :: [String.t()]} | :one_shot | :adapter_managed
Return the command and arguments to launch the agent subprocess.
The bridge uses this to open a Port. For one-shot adapters that manage
their own subprocess lifecycle, return :one_shot instead. For adapters
that keep persistent subprocesses but need to own more than one Port, return
:adapter_managed and implement handle_adapter_message/2.
@callback config_options() :: [map()]
Return the config options this agent supports.
Each option should follow the stable ACP select shape with "id", "name",
"type", "currentValue", "options", and optional "category" and
"description". Returned in session setup and config responses under
"configOptions".
Optional — defaults to an empty list.
Return environment variables for the child process.
Values are merged with adapter_opts[:env], with explicit adapter options
taking precedence. The bridge still clears known session-secret variables
before applying this environment.
Optional — defaults to an empty map.
@callback fork_session(params :: map(), state()) :: {:ok, map(), state()} | {:error, any(), state()}
Fork an existing session.
Optional — adapters that implement it are automatically advertised as
supporting session/fork.
@callback handle_adapter_message(message :: term(), state()) :: {:messages, [map()], state()} | {:partial, state()} | {:skip, state()}
Handle raw messages for adapter-managed subprocesses.
The bridge calls this for messages it does not own, including Port data, exit-status, and close notifications. The adapter is responsible for routing writes back to the correct subprocess and may return ACP messages to emit.
Optional — only used by adapters whose command/1 returns
:adapter_managed.
Initialize adapter state from options.
@callback list_sessions(params :: map(), state()) :: {:ok, [map()], state()} | {:error, any(), state()}
List available sessions for this agent.
Returns {:ok, sessions, new_state} where sessions is a list of maps
with "sessionId", "cwd", and optional "title", "updatedAt".
params is the decoded session/list params map, including optional
"cwd" and "cursor" values.
Optional — adapters that implement it are automatically advertised as
supporting session/list.
@callback modes() :: [map()]
Return the operational modes this agent supports.
Each mode is a map with "id", "name", and optional "description".
Returned in session setup responses under "modes".
Optional — defaults to an empty list.
Called after the Port is opened, before any ACP messages are processed.
Return {:ok, iodata, new_state} to write initial data to the port
(e.g., a JSON-RPC initialize handshake), or {:ok, state} to do nothing.
Optional — defaults to no-op.
Clean up adapter-managed resources before the bridge exits.
Optional — defaults to no-op.
@callback translate_inbound(raw_line :: String.t(), state()) :: {:messages, [map()], state()} | {:messages_and_write, [map()], iodata(), state()} | {:skip_and_write, iodata(), state()} | {:partial, state()} | {:skip, state()}
Translate one line of native CLI output to zero or more ACP messages.
Returns:
{:messages, [map()], new_state}— one or more ACP JSON-RPC messages{:messages_and_write, [map()], iodata(), new_state}— messages + data to write back to port{:skip_and_write, iodata(), new_state}— no messages, but write data back to port{:partial, new_state}— line accumulated, no complete messages yet{:skip, new_state}— line ignored (non-JSON, irrelevant event, etc.)
@callback translate_outbound(acp_message :: map(), state()) :: {:ok, iodata(), state()} | {:ok, :skip, state()} | {:ok, :pending, state()} | {:reply, result :: map(), state()} | {:messages, messages :: [map()], state()} | {:messages_and_reply, messages :: [map()], result :: map(), state()} | {:messages_and_write, messages :: [map()], iodata(), state()} | {:reply_and_write, result :: map(), iodata(), state()} | {:error, reason :: any(), state()} | {:one_shot, function(), state()}
Translate an outbound ACP JSON-RPC message to the native CLI format.
Returns {:ok, iodata, new_state} to write data to stdin,
or {:ok, :skip, new_state} when no output is needed (e.g., initialize
is handled internally by the bridge), or {:reply, result, new_state}
when the adapter can produce the JSON-RPC result directly, or
{:messages, messages, new_state} when the adapter can emit ACP messages
without writing to the native process, or
{:messages_and_reply, messages, result, new_state} when the adapter can
emit ACP messages before producing the JSON-RPC result directly, or
{:messages_and_write, messages, iodata, new_state} when the adapter can
emit ACP messages while also writing to the native process, or
{:reply_and_write, result, iodata, new_state} when it can reply while
also forwarding data to the agent process, or
{:error, reason, new_state} when the request can't be honored (e.g., a
config value outside the adapter's enum). The bridge translates
{:error, _, _} into a JSON-RPC error response back to the ACP client.
Adapter-managed implementations may return {:ok, :pending, state} after
writing to one of their own subprocesses when the ACP response will be
emitted later from handle_adapter_message/2.