# MCP Usage Guide

Source: https://docs.aon.pro/mcp/guide

> Derived from the same AON Docs release as the source page.

How to run a multi-turn shopping conversation on top of `aon_search_offers`: act on the server's follow-up suggestions, keep the session continuous, retry safely, and understand what happens when arguments are imperfect.

## The recommended flow

1.  **Vague intent?** Call `aon_get_category_schema` (or `aon_resolve_category` first if you have no category id) and ask the user one clarifying question from its decision factors.
2.  **Search.** Call `aon_search_offers` with one natural sentence in `intent.content`. Add `constraints.category_ids` only with ids you got from the other two tools — never invent them.
3.  **Present offers.** Show the offer title and description from `offer_info`, the merchant or provider from `entity.name`, and the `action.payload.url` link verbatim. `match_reason` tells you why each offer matched — use it in your presentation.
4.  **Offer the follow-ups.** If the response has `data.engagement`, its `refinements` and `followup_topics` are pre-validated, inventory-backed next questions: the server only suggests moves that have inventory behind them.

## The guidance loop

Each entry in `engagement.refinements[]` and `engagement.followup_topics[]` carries a `label` (show it to the user) and a `query_helper`:

```json
{
  "label": "Under $200",
  "query_helper": {
    "request_patch": { "intent": { "signals": { "budget": { "max": 200, "currency": "USD" } } } },
    "origin": [{ "kind": "query_helper", "id": "qh_..." }]
  }
}
```

Budget is an `intent.signals` value, so every budget helper includes both a numeric `max` and an uppercase ISO 4217 `currency`. The server may only emit one when the current request has a known currency; it must not infer a currency from the user's language or region.

Category ids remain canonical filters under `constraints`, not natural-language intent. A category-complement helper therefore patches a different branch of the next request:

```json
{
  "label": "Hiking socks",
  "query_helper": {
    "request_patch": { "constraints": { "category_ids": ["cat_hiking_socks"] } },
    "origin": [{ "kind": "query_helper", "id": "qh_..." }]
  }
}
```

When the user picks a suggestion, three things go into your next `aon_search_offers` call:

1.  **Merge the patch.** `request_patch` is an RFC 7386 merge-patch whose keys are protocol field paths — merge it straight into your previous request object. It can patch `intent` or `constraints`; do not move canonical category ids into `intent` just to make the JSON shape uniform.
2.  **Echo the origin.** Copy `query_helper.origin` into `intent.origin[]`. This is how adoption is attributed; skip it and the loop's analytics — and your ranking benefit — are lost.
3.  **Chain the round.** Pass the previous response's `data.request_id` as `context.session.previous_request_id` so offers the user already saw are not repeated.

## Session continuity

-   `context.session_id` — send a stable id per conversation. Cross-round offer de-duplication only engages when both a session id and a user identity are present.
-   User identity — hosts that proxy many end users should send the `x-aon-user-pseudo-id` HTTP header per user. The `context.user_profile.user_pseudo_id` field is a fallback for clients that cannot set headers; the header always wins. Single-user MCP clients (Claude Code, Cursor, a personal desktop) can skip this entirely.
-   `context.session.recent_topics[]` — up to 10 short phrases of recent conversation context; helps the server rank follow-up topics.

## `request_id` idempotency

`request_id` is an idempotency key with a **10-minute window**. Two obligations:

1.  **Retry = same id, new query = new id.** Reusing an old id within the window returns the previous full response _without an error_ — a changed question with a stale id silently gets the stale answer.
2.  **Never share ids across end users.** Under one API key, two users reusing one id would receive each other's responses — including each other's tracking links, which mis-attributes clicks and conversions.

Omit `request_id` and every call is treated as new (the server generates one for the response; the generated id does not join the idempotency window).

## Tolerant degradation

The search arm is forgiving on purpose: **a value-level problem never fails the call**. A wrong enum value, an out-of-range number, a malformed field — each is treated as "not sent" and reported in `extra.hints[]` with a machine-readable code and the closest valid option. If you see hints, read them: they tell you exactly how to fix the next call.

Only four things hard-fail:

1.  Failed authentication (bad or revoked key, wrong `X-AON-Test` usage)
2.  Invalid JSON
3.  Missing `intent.content`
4.  Oversized request body

Two more behaviors worth knowing:

-   **Unknown category ids** in `constraints.category_ids` come back as an in-band correction listing valid candidates — resolve ids through `aon_resolve_category` to avoid the round-trip.
-   **An empty `offers` list is a real answer.** Check `data.empty_reason`, relay it honestly, and offer the engagement follow-ups instead of retrying the same query.

## Paging through a catalog

For browse-style UIs, `pagination` (AON private extension) supports `limit` up to 50 with `offset` stepping — `offset: 0`, `50`, `100`, … The response does not report a total count; keep paging until a short or empty page comes back.

## Do / don't

| Do | Don't |
| --- | --- |
| Surface `action.payload.url` links verbatim | Rewrite, shorten, or proxy tracking links |
| Echo `query_helper.origin` when a suggestion is adopted | Apply a patch but drop its origin |
| Send one natural sentence as intent | Stuff keyword lists into `intent.content` |
| Use ids from `aon_resolve_category` | Guess or invent taxonomy ids |
| Show an honest "nothing relevant" on empty results | Pad with unrelated offers |

[Rate limits & FAQ →](https://docs.aon.pro/mcp/faq)
