# acpPlugin

[← All plugins](/docs/reference/plugins)

```ts
import { acpPlugin } from '@routecraft/ai'
```

Serves the [Agent Client Protocol](/docs/advanced/talk-from-your-editor) 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.

```ts
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](/docs/reference/configuration#acp). 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:

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

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

**Options:**

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `path` | `string` | `'/acp'` | HTTP path the protocol is mounted on |
| `server` | `string` | `'default'` | Named entry under `defineConfig({ servers })` to mount on |
| `auth` | `HttpAuth \| false` | inherits `servers.<name>.auth` | Auth for the mount, the same union `mcpPlugin` takes. `false` removes the wall while keeping the inherited validator reachable |
| `agent` | `string` | the only registered agent | The agent a harness serves when it names none |
| `cors` | `HttpCorsOptions \| false` | loopback-only | Response-header policy and additional Origin refusal. `false` delegates CORS handling to the proxy; Host and browser-admission checks remain enforced. |
| `browserOrigins` | `readonly 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 |
| `toolCallPayloads` | `boolean` | `true` | Whether 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`](/docs/reference/cli#acp) 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](/docs/advanced/talk-from-your-editor#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](/docs/advanced/talk-from-your-editor#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`](/docs/reference/cli#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](/docs/advanced/securing-capabilities#cors) 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](/docs/reference/events#acp-plugin-events).

## 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`](/docs/reference/adapters/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](/docs/reference/adapters/surface#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](/docs/reference/errors#rc-5017) naming the package to install.
