acpPlugin

import { acpPlugin } from '@routecraft/ai'

Serves the Agent Client Protocol over Streamable HTTP, so a person can talk to this instance's agents from their editor. It mounts on a named server beside the MCP mount and the HTTP surface, behind the same wall as everything else the instance exposes.

Nothing ships switched off. Installing the plugin serves the protocol, and the framework's own name reaches the editor unless the app replaces it.

import { defineConfig } from '@routecraft/routecraft'
import '@routecraft/ai'

export const craftConfig = defineConfig({
  servers: {
    default: { port: 8080 },
  },
  acp: {},
})

The acp key is the first-party form, beside mcp, and takes the same options as the factory. It serves the agents craft start discovers under agents/ or the ones written under agent, in any key order: see the configuration reference. An instance holding no agents when the mount applies fails the build with RC5003, so a project with no agents needs one before the mount has anything to serve.

The factory is for plugins, where it must follow the agentPlugin() that registers the agents:

import { defineConfig } from '@routecraft/routecraft'
import { acpPlugin, agentPlugin } from '@routecraft/ai'

export const craftConfig = defineConfig({
  servers: {
    default: { port: 8080 },
  },
  plugins: [
    agentPlugin(),
    acpPlugin(),
  ],
})

Options:

OptionTypeDefaultDescription
pathstring'/acp'HTTP path the protocol is mounted on
serverstring'default'Named entry under defineConfig({ servers }) to mount on
authHttpAuth | falseinherits servers.<name>.authAuth for the mount, the same union mcpPlugin takes. false removes the wall while keeping the inherited validator reachable
agentstringthe only registered agentThe agent a harness serves when it names none
corsHttpCorsOptions | falseloopback-onlyResponse-header policy and additional Origin refusal. false delegates CORS handling to the proxy; Host and browser-admission checks remain enforced.
browserOriginsreadonly string[][]Exact HTTP(S) origins allowed to send requests, including port. Required for local browser tools too; independent of CORS.
agentInfo{ name?, title?, version? }{ name: 'routecraft', title: 'Routecraft', version }What the editor calls this agent
toolCallPayloadsbooleantrueWhether a tool call's arguments and result reach the editor

Order matters in plugins, and it is checked

The mount builds one route per registered agent when it applies, so it has to apply after the agents are registered. The acp key does this on its own: config keys apply in a fixed order with acp last, before anything in plugins. As a plugin in plugins, acpPlugin() has to be listed after agentPlugin(). A context whose agent registry is empty at that moment fails the build with a message saying so, rather than serving a protocol with nothing behind it.

Which agent a client talks to

Unset, agent resolves to the only agent the context holds. A context holding several refuses session/new with a message naming them all, because silently picking one for somebody would answer in the wrong voice with no way to notice.

A harness names its own with the Routecraft-Agent request header, which is what craft acp --agent sends, and every session it opens belongs to that agent for life. A name the instance does not hold is refused by name. There is no config option and no mode for this: see why agent selection is not an ACP mode.

Every agent the context holds is advertised to every credential holder. There is no per-agent visibility rule: agents are not what carries the security here, hands are, and each one authorizes the caller on every call under the principal of the person who typed the prompt.

A prompt sent while a turn runs

A session/prompt that arrives while the conversation's turn is running is queued behind it and answered, together with anything else that queued, by the turn that runs next. The request stays open until that reply has streamed and returns end_turn only then; several held on one conversation return in the order they were sent. A prompt this instance cannot answer returns cancelled, never end_turn with nothing shown. session/cancel interrupts the running turn alone: its request returns cancelled and a queued message is still answered by the next turn. The full behaviour is in Sending while a turn runs.

Who can see a conversation

A conversation belongs to the person who started it. The mount lists, loads and resumes only the conversations whose owner matches the caller's subject; somebody else's session id is answered as if it did not exist, so probing for one tells the prober nothing. A mount with no wall has no subjects to tell apart, and every conversation on it belongs to the same unnamed person.

That answer is the protocol's own resource-not-found code (-32002), one code and one message for a conversation that is missing, somebody else's, or another agent's. A client may drop a conversation from its tracking on that code and on no other: a refusal of any other kind (invalid params for a request that was wrong, an internal error for an instance that is unwell) means the conversation may well still exist, and craft acp keeps it and tries again on its next reconnection. The whole taxonomy is recorded in the error policy beside the code, so the bridge and the mount cannot drift on it.

Withholding tool payloads

toolCallPayloads: false keeps the arguments and the result of every tool call out of the editor. The call itself stays visible whatever this says: a person watching a turn still sees which hands ran and how each one ended. This is the expandable detail underneath.

A hand that fails is under the same policy. With payloads shown, the failed call carries the error's message, so the person reads the same reason the model reads; with payloads withheld it carries the error's class and code and nothing else, because a handler's message routinely echoes the argument it rejected, which is what the policy exists to keep out of the editor.

An instance serving people other than its operator should consider setting it. A hand's arguments routinely carry a credential, a customer's address or a mail body, and an editor is a third program that renders and logs whatever it is handed.

What the mount does not check yet

ACP and MCP share a Host and browser-origin gate in core's web ingress. It runs before preflight, authentication and protocol dispatch. Missing, malformed and foreign Host values return 403 even with cors: false or no auth. A reverse proxy that preserves a public Host needs that hostname in servers.<name>.allowedHostnames; forwarding headers never add trusted names.

Requests carrying Origin require an exact entry in browserOrigins, including local browser tools. CORS is an additional policy: it can refuse an admitted origin, but never grants admission. With cors: false, the proxy owns CORS headers while the Host and explicit browser-origin gates stay active. Editor clients without Origin need no browser configuration. Host and Origin checks do not replace authentication.

See Securing capabilities for local-browser and reverse-proxy configuration.

Branding

agentInfo is replaced outright rather than merged, so somebody who deliberately white-labels gets what they asked for rather than the framework's name in the field they did not set.

The protocol carries no icon, logo or image field anywhere in version 1, so those two strings are the entire branding surface available here. That is the protocol's limit rather than a choice of ours.

What it emits

Connections and conversations announce themselves on the event bus: plugin:acp:connection:opened, plugin:acp:connection:closed and plugin:acp:session:attached. A connection that broke rather than closed is the same closed event carrying fault. See the events reference.

Reaching the person back

A capability running inside a turn can ask the editor a question or push a notice to it with the surface adapter. It works only for an exchange that started at an editor, and it says so rather than guessing when it did not. What a cancel does to a call in flight, and how a route cleans up after one, is on that page under when the turn is cancelled.

The SDK is an optional peer

The mount is built on @agentclientprotocol/sdk, declared as an optional peer dependency. An app that installs acpPlugin() without it fails with RC5017 naming the package to install.