> ## Documentation Index
> Fetch the complete documentation index at: https://www.commercengine.io/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# AI Agent Tools

> Expose catalog, session, cart, navigation, and storefront content capabilities to browser AI agents through WebMCP.

`@commercengine/ai` exposes Commerce Engine storefront capabilities to browser AI agents through WebMCP. It works through the Storefront SDK and your existing checkout/navigation integrations rather than asking an agent to infer your UI or click DOM elements.

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
pnpm add @commercengine/ai @commercengine/storefront
```

Add `@commercengine/checkout` only if you use Hosted Checkout and want drawer tools such as `open_cart`, `open_checkout`, or `open_login`.

## The safety boundary

The package intentionally lets an agent prepare a purchase but never complete one.

| Agent can                                                 | Agent cannot                                                      |
| --------------------------------------------------------- | ----------------------------------------------------------------- |
| Search and browse products                                | Enter or request a password or OTP                                |
| Inspect product and variant data                          | Choose payment details                                            |
| Read the current cart                                     | Enter shipping/payment credentials                                |
| Add, update, and remove cart lines                        | Place an order                                                    |
| Open a product or orders page                             | Complete checkout on the shopper's behalf                         |
| Open cart, login, or checkout UI when those bridges exist | Access raw customer identity, addresses, tokens, or payment state |

`open_checkout` hands control back to the shopper. Commerce Engine deliberately provides no payment or order-placement tool.

## Quick start

Register from client code:

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { registerCommerceWebMcp } from "@commercengine/ai/webmcp";
import { routes, site } from "./commerce-seo.config";
import { storefront } from "./storefront";

const registration = await registerCommerceWebMcp({
  storefront,
  siteUrl: site.url,
  routes,
  diagnostics: import.meta.env.DEV
    ? (event) => console.info("[commerce-ai]", event.code, event.message ?? "")
    : undefined,
});

// Save the controller and abort it when this root runtime/component unmounts.
// registration?.abort();
```

`site` and `routes` should come from the same `defineCommerceSeoConfig()` declaration used by `@commercengine/seo`. That keeps crawler-visible and agent-visible URLs identical.

<Info>
  Registration returns `null` when WebMCP is not available. That is an expected capability check, not a storefront error. In development, diagnostics prove the registration code actually ran.
</Info>

## Capability gating

Tools appear only when the capability behind them exists.

| Capability                       | Tools                                                                                        |
| -------------------------------- | -------------------------------------------------------------------------------------------- |
| Always                           | `search_products`, `get_product`, `browse_store`, `get_variant`                              |
| Session-capable storefront       | `get_session_state`, `get_cart`, `add_to_cart`, `set_cart_item_quantity`, `remove_from_cart` |
| `navigation`                     | `open_product`                                                                               |
| `navigation.ordersUrl`           | `manage_orders`                                                                              |
| Hosted Checkout bridge           | `open_cart`, `open_checkout`                                                                 |
| Checkout bridge with `openLogin` | `open_login`                                                                                 |
| `shopContent`                    | `search_shop_policies_and_faqs`                                                              |

Which adds up to a predictable tool count:

| Configuration                   | Tools |
| ------------------------------- | ----- |
| `session: null`, nothing else   | 4     |
| Catalog + session (the default) | 9     |
| + Hosted Checkout bridge        | 11    |
| + `checkout.openLogin`          | 12    |
| + `navigation`                  | 13    |
| + `navigation.ordersUrl`        | 14    |
| + `shopContent`                 | 15    |

The storefront's browser session client is used automatically by default, discovered through its `clientStorefront()` or `session()` accessor. A storefront exposing neither registers the four catalog tools and nothing else — indistinguishable from a bug, so check the accessor name first when cart tools are missing.

Tool registration does **not** wait for anonymous-session bootstrap: session/cart tools can be declared immediately and return a retryable not-ready result until the session becomes usable. Set `session: null` when you intentionally want a catalog-only agent surface.

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
await registerCommerceWebMcp({
  storefront,
  siteUrl: site.url,
  routes,
  session: null,
});
```

## Add Hosted Checkout and navigation

Hosted Checkout contributes presentation only. Cart reads and mutations use the authenticated Commerce Engine Storefront SDK and return the resulting cart directly.

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { createHostedCheckoutBridge } from "@commercengine/ai/checkout";
import { registerCommerceWebMcp } from "@commercengine/ai/webmcp";
import { getCheckout } from "@commercengine/checkout";

await registerCommerceWebMcp({
  storefront,
  siteUrl: site.url,
  routes,
  checkout: createHostedCheckoutBridge({
    getState: () => getCheckout(),
  }),
  navigation: {
    navigate: (url) => router.push(url),
    ordersUrl: "/account/orders",
  },
});
```

The checkout bridge never creates a session or owns cart truth. `open_cart` opens the existing drawer; the drawer then refetches so it reflects any mutation the agent made through the SDK.

## Cart semantics

Cart tools operate on the shopper's real Commerce Engine cart, including anonymous shoppers.

### Add versus set

* `add_to_cart` **adds** its quantity to the current quantity. It can take up to 10 items in one invocation.
* `set_cart_item_quantity` sets the **absolute paid quantity** for an existing cart line.
* setting quantity to `0` removes the line
* `remove_from_cart` is the explicit removal form

Do not treat `add_to_cart` as an absolute setter. Repeating an add can create duplicate quantity.

### Ordering constraints

Cart and catalog output include:

* `minOrderQuantity`
* `maxOrderQuantity`
* `incrementalQuantity`

Existing cart lines are checked against their constraints before mutation. New lines rely on Commerce Engine's server validation because the cart does not yet contain the line's constraints.

### Promotional free items

A promotion can add units the shopper never selected. Agent-facing cart lines distinguish:

| Field           | Meaning                                          |
| --------------- | ------------------------------------------------ |
| `quantity`      | paid units; cart mutations operate on this value |
| `freeQuantity`  | promotion-granted units priced at zero           |
| `totalQuantity` | paid + free units displayed to the shopper       |
| `removable`     | `false` for a completely promotional line        |

A free-only line cannot be removed or reduced, matching the Hosted Checkout UI.

## Result shapes

Tools return plain values. Do not JSON-stringify them yourself; the user agent serializes the result.

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  ok: true,
  data: {
    productId: "..."
  }
}
```

Failures distinguish retryable infrastructure conditions from terminal application conditions:

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  ok: false,
  error: {
    code: "product_not_found",
    message: "Product not found",
    retryable: false
  }
}
```

A successful cart mutation returns a safe projection of the resulting cart. The cart projection is an allow-list, so future Storefront API fields such as addresses, metadata, or payment state cannot accidentally become agent-visible.

## Two signals, doing different jobs

Claiming a tool set is not atomic — it is a sequence of awaited `registerTool` calls. Two overlapping registrations against one model context would race for the same names and one would throw `InvalidStateError: Duplicate tool name`. React Strict Mode makes this the ordinary case rather than an edge one: it mounts, cleans up, and mounts again back to back on every dev mount.

Registrations against one model context are therefore serialized — a second call waits for the first to settle, including any rollback.

| Signal                  | Cancels                                                                              | Why the other cannot cover it                            |
| ----------------------- | ------------------------------------------------------------------------------------ | -------------------------------------------------------- |
| The returned controller | Tools that **did** register                                                          | It does not exist until the promise resolves             |
| `config.signal`         | A registration still **in progress**, including the `registerTool` currently awaited | The controller cannot reach a pass that has not finished |

Cancellation rejects with `CommerceWebMcpAbortError`, which callers are expected to swallow — it means "you asked for this". Without `config.signal`, cleanup can run before the controller exists and the stale pass stays live after its component has gone away.

## Resolving CMS slugs in both directions

`routes.product` runs in the browser during tool execution, so a resolver that performs a CMS lookup would need that credential on the client. The portable shape is a pure function over route data the browser already has:

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { createCommerceRouteManifest } from "@commercengine/seo/routes";

const manifest = createCommerceRouteManifest(await loadRouteRecords());

export const routes = {
  // Outbound: entity → URL. Never guess; null means "no public page".
  product: (input) => manifest.productPath(input),

  // Inbound: public slug → CE identifier. The `?? publicSlug` tail is valid only
  // because this storefront also answers to catalog slugs. Return null instead
  // where CMS slugs are the only accepted public form.
  resolveProductRoute: (publicSlug) => manifest.resolveProduct(publicSlug)?.productId ?? publicSlug,
};
```

`resolveProductRoute` matters as much as `product`. An agent reads a CMS page slug off the page and calls `get_product("knee-pain-relief-oil")`; without the inbound mapping that goes straight to a catalog which has never heard of it.

The two directions have **opposite** fallback rules, and conflating them is the common mistake:

| Direction                       | On no match                                                                          | Why                                                                                                   |
| ------------------------------- | ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- |
| Outbound `productPath()`        | Return `null` and render a non-link                                                  | A guessed `/products/{catalog-slug}` is a shopper-facing 404                                          |
| Inbound `resolveProductRoute()` | `?? publicSlug` only when catalog slugs are also valid public URLs; otherwise `null` | The cost is one failed lookup, and the fallback rescues a page published since the last cache refresh |

Returning `null` from an inbound resolver fails the tool with `route_not_found` **before** any catalog request rather than passing an unmapped slug to a catalog that has never heard of it.

<Warning>
  Do not ship a complete 100,000-product manifest to the browser merely to resolve a search page. This package performs no route-data transport and imposes no API, batching, or cache policy — at that size, back the same hooks with a storefront-owned point cache that loads only the result set.
</Warning>

## Cancellation is not rollback

WebMCP execution may supply an `AbortSignal`, and the package honors it at asynchronous boundaries. Commerce Engine SDK requests do not currently accept that signal, so once a cart request is in flight, cancellation cannot undo a server-side mutation.

If the mutation response already confirms the new cart, the package returns that truth instead of pretending the change never happened. This prevents an agent from retrying an already-applied add and duplicating quantity.

## Anonymous and authenticated sessions

A shopper does not need to sign in to use cart tools. An anonymous Commerce Engine session has a user identity sufficient for cart ownership.

Signing in adds customer capabilities such as orders, saved addresses, loyalty, and customer pricing. The AI package does not authenticate the shopper itself:

* `open_login` only opens your login UI
* `get_session_state` reports whether the current session is anonymous/logged in and whether login UI is available
* no tool accepts a password or OTP
* session state does not expose who the shopper is

A guest cart survives sign-in in the same browser session because the session token evolves in place. Do not promise cart merge across a different device or browser.

## Navigation security

Navigation is same-origin by default. External destinations require explicit opt-in:

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
navigation: {
  navigate: (url) => router.push(url),
  allowedExternalOrigins: ["https://support.acme.example"],
}
```

Permission is not inferred from arbitrary catalog or CMS content. This prevents attacker-controlled product content from becoming an open redirect through an agent tool.

## Catalog and CMS content is untrusted

Catalog/search and optional policy/FAQ results are marked as untrusted content for the agent. Tool descriptions remain package-authored and are never interpolated from CMS output.

Free-form input is bounded, tool schemas reject extra properties, and sensitive cart/customer fields are not forwarded by default.

<Card title="Agent tools and security" icon="shield-halved" href="/docs/seo-ai/agent-security">
  Full tool inventory, diagnostics, WebMCP availability, extension modules, and the security model.
</Card>

## Development diagnostics

Always wire diagnostics during development:

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
diagnostics: (event) =>
  console.info("[commerce-ai]", event.code, event.message ?? "")
```

Typical outcomes include:

```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
unsupported    modelContext is not available in this browser
registered     individual tool registered
registered:12  complete tool set registered
rollback       partial registration failed and was removed
```

No diagnostic line at all usually means your mount code never executed. Typecheck, build, and lint cannot detect dead registration code.

## Build tools without the DOM

Use `createCommerceAiTools(config)` when you want to inspect or test the exact tool set without browser registration:

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { createCommerceAiTools } from "@commercengine/ai";

const tools = createCommerceAiTools({
  storefront,
  siteUrl: site.url,
  routes,
});

console.log(tools.map((tool) => tool.name));
```

Tools are composed from plain modules. `extraModules` appends custom capabilities, while `modules` replaces the default module list entirely. Duplicate tool names fail during composition.

## Key exports

| Import                       | Use                                                    |
| ---------------------------- | ------------------------------------------------------ |
| `@commercengine/ai`          | tool composition, result helpers, types, route helpers |
| `@commercengine/ai/webmcp`   | browser WebMCP registration                            |
| `@commercengine/ai/checkout` | adapter for an existing Hosted Checkout instance       |

## Framework setup

Registration is always client-side. The correct mount point differs by framework:

<CardGroup cols={2}>
  <Card title="React / Vite" icon="react" href="/docs/seo-ai/react">
    Register from app bootstrap or a root effect.
  </Card>

  <Card title="Next.js" icon="n" href="/docs/seo-ai/nextjs">
    Use a root Client Component.
  </Card>

  <Card title="TanStack Start" icon="code" href="/docs/seo-ai/tanstack-start">
    Use a root client initializer.
  </Card>

  <Card title="Astro" icon="star" href="/docs/seo-ai/astro">
    Register on `astro:page-load` and handle View Transitions.
  </Card>

  <Card title="SvelteKit" icon="code" href="/docs/seo-ai/sveltekit">
    Register in root `onMount`.
  </Card>
</CardGroup>
