ExMCP.ACP.Client.Handler behaviour (ex_mcp v1.3.0)

Copy Markdown View Source

Behaviour for handling ACP session events and agent requests.

Implement this behaviour to customize how your application responds to streaming session updates, permission requests, and file access requests from ACP agents.

See ExMCP.ACP.Client.DefaultHandler for a reference implementation.

Session updates and permission requests each have a legacy callback and a context-aware variant that also receives the decoded JSON-RPC message (handle_session_update/3 or handle_session_update/4, and handle_permission_request/4 or handle_permission_request/5). Both arities are optional so a handler can implement only the variant it needs, but it must implement at least one of each pair: the client refuses to start a handler that implements neither, with {:handler_init_failed, {:missing_callback, name}}. When both are present the context-aware variant is called.

Summary

Callbacks

Called when an accepted URL elicitation completes out of band.

Called when the agent requests to read a file.

Called when the agent requests to write a file.

Called when the agent requests a form-mode elicitation.

Called when the agent requests permission to use a tool.

Handles a permission request with the decoded JSON-RPC message received by the ACP client.

Called for each session/update notification from the agent.

Handles a session update with the decoded JSON-RPC message received by the ACP client.

Called when the agent requests a terminal operation.

Called when the agent requests a URL-mode elicitation.

Called when the handler is initialized.

Called when the handler is being terminated.

Types

state()

@type state() :: any()

Callbacks

handle_elicitation_complete(elicitation_id, state)

(optional)
@callback handle_elicitation_complete(elicitation_id :: String.t(), state()) ::
  {:ok, state()}

Called when an accepted URL elicitation completes out of band.

handle_file_read(session_id, path, opts, state)

(optional)
@callback handle_file_read(
  session_id :: String.t(),
  path :: String.t(),
  opts :: map(),
  state()
) ::
  {:ok, content :: String.t(), state()}
  | {:error, reason :: String.t(), state()}

Called when the agent requests to read a file.

Return {:ok, content, state} with the file contents, or {:error, reason, state} to deny access.

handle_file_write(session_id, path, content, state)

(optional)
@callback handle_file_write(
  session_id :: String.t(),
  path :: String.t(),
  content :: String.t(),
  state()
) :: {:ok, state()} | {:error, reason :: String.t(), state()}

Called when the agent requests to write a file.

Return {:ok, state} to allow the write, or {:error, reason, state} to deny it.

handle_form_elicitation(params, state)

(optional)
@callback handle_form_elicitation(params :: map(), state()) ::
  {:ok, response :: map(), state()} | {:error, reason :: term(), state()}

Called when the agent requests a form-mode elicitation.

handle_permission_request(session_id, tool_call, options, state)

(optional)
@callback handle_permission_request(
  session_id :: String.t(),
  tool_call :: map(),
  options :: [map()],
  state()
) :: {:ok, outcome :: map(), state()}

Called when the agent requests permission to use a tool.

Must return an outcome map with an "optionId" matching one of the provided options.

handle_permission_request(session_id, tool_call, options, message, state)

(optional)
@callback handle_permission_request(
  session_id :: String.t(),
  tool_call :: map(),
  options :: [map()],
  message :: map(),
  state()
) :: {:ok, outcome :: map(), state()}

Handles a permission request with the decoded JSON-RPC message received by the ACP client.

When implemented, this optional callback is called instead of handle_permission_request/4. The message includes the original request ID and all received fields. ExMCP retains request correlation, validation, cancellation, and timeout ownership. Return the same outcome as the legacy callback; do not send a JSON-RPC response from the handler. This callback has the same ACP-boundary limit as handle_session_update/4: an adapted agent can supply only the fields that its adapter placed in the ACP request.

handle_session_update(session_id, update, state)

(optional)
@callback handle_session_update(session_id :: String.t(), update :: map(), state()) ::
  {:ok, state()}

Called for each session/update notification from the agent.

The update map contains a "sessionUpdate" discriminator field indicating the update type (e.g., "agent_message_chunk", "tool_call", "plan", etc.).

Optional when handle_session_update/4 is implemented.

handle_session_update(session_id, update, message, state)

(optional)
@callback handle_session_update(
  session_id :: String.t(),
  update :: map(),
  message :: map(),
  state()
) :: {:ok, state()}

Handles a session update with the decoded JSON-RPC message received by the ACP client.

When implemented, this optional callback is called instead of handle_session_update/3. The message retains unknown top-level and parameter fields from the ACP message. It is the decoded map, not the original JSON bytes.

This is an ACP-boundary value. A native ACP agent supplies the message. When the client uses ExMCP.ACP.AdapterTransport, ExMCP.ACP.AdapterBridge and the selected adapter construct the ACP message from the agent's native protocol. Native fields that the adapter does not map are not present.

ExMCP validates the update and session before dispatch. Message data counts toward the existing handler update queue byte limit.

handle_terminal_request(method, params, id, state)

(optional)
@callback handle_terminal_request(
  method :: String.t(),
  params :: map(),
  id :: integer() | String.t() | nil,
  state()
) :: {:ok, result :: map(), state()} | {:error, reason :: String.t(), state()}

Called when the agent requests a terminal operation.

The method is one of the stable terminal/* methods and params is the raw ACP params map. Return {:ok, result, state} with the method-specific result map, or {:error, reason, state} to deny or fail the operation.

handle_url_elicitation(params, state)

(optional)
@callback handle_url_elicitation(params :: map(), state()) ::
  {:ok, response :: map(), state()} | {:error, reason :: term(), state()}

Called when the agent requests a URL-mode elicitation.

init(opts)

@callback init(opts :: keyword()) :: {:ok, state()}

Called when the handler is initialized.

terminate(reason, state)

(optional)
@callback terminate(reason :: any(), state()) :: :ok

Called when the handler is being terminated.