# [Protocols \(UCP / ACP / MCP\)](https://rebilder.com/docs/protocols)

> Spec-versioned adapters that make a gateway-fronted store transactable: UCP discovery, catalog, and checkout handoff; an ACP product feed; and an MCP tool server. All wire in through one `createProtocolHandler` call.

- **Updated:** 2026-08-12
- **Author:** Rebilder
- **Section:** Concepts
- **Description:** Spec-versioned adapters that make a gateway-fronted store transactable: UCP discovery, catalog, and checkout handoff; an ACP product feed; and an MCP tool server. All wire in through one createProtocolHandler call.
- **Publisher:** Rebilder

## What ships in v0

`@rebilder/protocols` implements the three agentic-commerce protocols as thin, **spec-versioned adapters** over the same source-of-truth wiring the gateway uses:

- **UCP**: discovery (`/.well-known/ucp`), a paginated catalog, and a checkout handoff onto your own PSP rails.
- **ACP**: a product feed of schema.org Product records, byte-identical to the structured data your canonical pages embed.
- **MCP**: a JSON-RPC 2.0 server exposing `search_catalog`, `get_product`, and `get_policies` tools.

The adapters assemble responses from your configured sources. Their runtime uses no model; source resolver latency depends on your implementation. Mumm’s negotiation interface is a separate versioned contract.

> **Supported protocol versions** The `v0` directories are **our v0 adapters, tracking the external spec surfaces as observed in August 2026** (the MCP adapter pins protocol revision `2025-06-18`; the UCP/ACP wire shapes are our pinned v0 tracking of those young specs). These specs churn quarterly. When a spec moves, a new version directory lands and `v0` keeps serving exactly these shapes; "we track spec changes so you don't" is the product.

## The endpoint map

| Endpoint | Adapter | Behavior |
| --- | --- | --- |
| `GET /.well-known/ucp` | `ucp/v0` | Discovery document: store id/origin, capabilities, endpoint URLs. Advertises `checkout_handoff` only when `config.checkout` is wired. |
| `GET /.well-known/ucp/v0/catalog` | `ucp/v0` | Paginated catalog (`?limit=` 1–250, default 50; `?cursor=` from the previous page). Items are exact field copies: `url`, `title`, `price {amount, currency}` (minor units, verbatim), `availability`. |
| `POST /.well-known/ucp/v0/checkout` | `ucp/v0` | Body `{ "product_url": "…" }` → `{ handoff_url }` onto your own checkout/PSP rails. `403 verification_required` unless the request carries the gateway-stamped verified verdict or you opted out; see the verification gate below. `404` when checkout is unconfigured or your handoff declines the product. |
| `GET /ucp/v0/catalog`, `POST /ucp/v0/checkout` | `ucp/v0` | Direct-mount aliases of the two above. Discovery always advertises the `/.well-known/` forms. |
| `GET /acp/v0/feed` | `acp/v0` | Product feed: schema.org Product records, byte-identical to the canonical page’s structured data. A record whose source data fails validation is dropped, never patched. |
| `POST /mcp` | `mcp/v0` | Single-shot JSON-RPC 2.0: `initialize`, `ping`, `tools/list`, `tools/call` with tools `search_catalog {query}`, `get_product {url}`, `get_policies {}`. Standard JSON-RPC error codes; notifications get `202`; batches are rejected (`-32600`); `GET /mcp` is `405` (no SSE stream in v0). |

Every response carries `X-Rebilder-Protocol: <proto>/v0` and every payload carries `spec_version` (`"v0"`). Anything not in the table returns `null`: the request falls through to your normal serving path, and an adapter throw is contained to `null` too: a protocol bug never breaks your site.

## Wiring through the gateway

The gateway deliberately does **not** depend on `@rebilder/protocols`: a store that only wants the markdown path shouldn’t carry protocol adapters, and spec versions must ship on their own cadence without version-bumping the gateway. You construct the handler and pass it in as the `protocols` hook; `ProtocolSources` is structurally identical to `GatewaySources`, so one wiring object serves both configs:

lib/gateway-config.ts

```
import { handleRequest, type GatewayConfig } from '@rebilder/gateway'
import { createProtocolHandler } from '@rebilder/protocols'

const sources = { product, policies, catalog } // ProtocolSources is structurally
                                               // identical to GatewaySources —
                                               // one wiring object serves both

const config: GatewayConfig = {
  storeId: 'store_123',
  sources,
  protocols: createProtocolHandler({
    storeId: 'store_123',
    sources,
    checkout: { handoffUrl: (productUrl) => merchantCheckoutUrlFor(productUrl) },
    // leave onEvent unset here — the gateway already emits one event per request
  }),
  onEvent: (event) => queue(event),
}
```

- The hook is invoked **only** when a request classifies onto the `protocol` path (see [Classification](/docs/classification)).
- A returned `Response` is served as-is, and the request’s event records `response.path: "protocol"` with measured `render_ms`.
- `null`, an unset hook, or a hook that throws all preserve the exact pass-through behavior, event included. Without the hook, protocol routes pass through and your [events](/docs/events) still record the demand.
- Leave `onEvent` unset on the protocol handler when it sits behind the gateway, because the gateway already emits one event per request; setting both double-counts.

## Standalone mount

No gateway (for example a dedicated protocol origin)? Mount the handler on any web-standard runtime and set `onEvent` yourself: every served protocol response (errors included) emits one `RebilderEventV0` with `response.path: "protocol"` and measured `render_ms`. Emission is fire-and-forget and can never break serving.

worker.ts

```
const protocols = createProtocolHandler({ storeId, sources, checkout, onEvent })
export default { fetch: async (req: Request) => (await protocols(req)) ?? new Response('Not found', { status: 404 }) }
```

## The checkout verification gate

The UCP checkout endpoint requires a **cryptographically verified agent by default** (`checkout.requireVerified`, default `true`). Verification is Web Bot Auth (RFC 9421 Ed25519 message signatures), run by the gateway when you inject a key registry; the gateway stamps its verdict on a cloned request as `x-rebilder-agent-verified: true | false` (client-sent values are always overwritten, so a spoofed verdict cannot survive the gateway):

lib/gateway-config.ts

```
import { handleRequest, type GatewayConfig, type AgentKeyRegistry } from '@rebilder/gateway'

const registry: AgentKeyRegistry = { /* operator-populated — configure trusted platform keys */ }

const config: GatewayConfig = {
  storeId: 'store_123',
  sources,
  protocols: createProtocolHandler({ storeId: 'store_123', sources, checkout }),
  verification: { keys: registry },   // require?: 'protocol' — the default and only v0 scope
}
```

- `x-rebilder-agent-verified: true` → the normal `{ handoff_url }` response.
- Anything else (header `false`, or absent) → `403` with `{"error": "verification_required"}`, checked before the body is even parsed.
- Read endpoints (discovery, catalog, ACP feed, MCP tools) are **never** gated; only the transaction is.

> **When verification is enforced** Populate `KNOWN_AGENT_DIRECTORY` with trusted platform keys to use verified checkout. Without a matching key, the default checkout returns `403 verification_required`. Setting `requireVerified: false` allows an open checkout handoff. In verified mode, mount the handler behind the configured gateway or an edge that strips inbound verdict headers; a client-supplied verdict is not a credential.

## Payments stay with your provider

> **Checkout is a redirect handoff, not a wallet** The checkout adapter returns a handoff URL to your configured payment provider. Payment authorization and execution take place there.

Connect prices, stock, titles and policies to authoritative business sources. Equivalent representations of an offer must agree. Correct invalid source data and revalidate it before serving; separately authorized negotiated offers can carry their own terms.

## Version pinning

- Every spec-version directory (`ucp/v0`, `acp/v0`, `mcp/v0`) carries its **own conformance suite**: golden request/response fixtures asserting the exact wire shapes. A version is not shippable until its conformance tests are green.
- **Upgrading a spec version = a new directory** (`ucp/v1`) with green conformance tests plus a deprecation note on the old one. A shipped version is never edited in place; any change that breaks its conformance suite is by definition a new version. Old versions keep serving until merchants migrate off them.
- Per-version import surface: subpath exports `@rebilder/protocols/ucp/v0`, `/acp/v0`, `/mcp/v0`. Deeper imports are forbidden.

The full export list is on the [API reference](/docs/reference#protocols-package). Orders that arrive through these endpoints join the funnel like any other; see [Outcomes](/docs/outcomes) for how attribution works.