MCP Usage Guide
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
- Vague intent? Call
aon_get_category_schema(oraon_resolve_categoryfirst if you have no category id) and ask the user one clarifying question from its decision factors. - Search. Call
aon_search_offerswith one natural sentence inintent.content. Addconstraints.category_idsonly with ids you got from the other two tools — never invent them. - Present offers. Show the offer title and description from
offer_info, the merchant or provider fromentity.name, and theaction.payload.urllink verbatim.match_reasontells you why each offer matched — use it in your presentation. - Offer the follow-ups. If the response has
data.engagement, itsrefinementsandfollowup_topicsare 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:
{
"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:
{
"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:
- Merge the patch.
request_patchis an RFC 7386 merge-patch whose keys are protocol field paths — merge it straight into your previous request object. It can patchintentorconstraints; do not move canonical category ids intointentjust to make the JSON shape uniform. - Echo the origin. Copy
query_helper.originintointent.origin[]. This is how adoption is attributed; skip it and the loop's analytics — and your ranking benefit — are lost. - Chain the round. Pass the previous response's
data.request_idascontext.session.previous_request_idso 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-idHTTP header per user. Thecontext.user_profile.user_pseudo_idfield 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:
- 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.
- 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:
- Failed authentication (bad or revoked key, wrong
X-AON-Testusage) - Invalid JSON
- Missing
intent.content - Oversized request body
Two more behaviors worth knowing:
- Unknown category ids in
constraints.category_idscome back as an in-band correction listing valid candidates — resolve ids throughaon_resolve_categoryto avoid the round-trip. - An empty
offerslist is a real answer. Checkdata.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 |