A Server for Sundry's Order System
Sundry's order system is an internal HTTP API with forty endpoints, owned by a team of six, built for a storefront and an admin console. It has no interest in being an agent's tool surface, and it should not have to be. The server that sits in front of it is a translation layer, and the translation — not the transport — is all of the work.
One rule makes the design tractable: derive the tool surface from what support actually does, not from what the API happens to expose. Forty endpoints become four tools that way. Published the other way round, they stay forty, and the agent spends its turns sequencing internal calls it should never have needed to know about.
Starting From the Transcript, Not the API Index
Vera read 200 resolved tickets before writing a line of the server, and marked every point where a human touched the order system. Four actions covered all of them. Find which order the buyer means, from whatever fragment they gave — an email address, a half-remembered id, "the one that came Tuesday". Get the full picture of one order. Get the item collected. Send another one. Everything else among the forty endpoints belongs to the storefront, to finance, or to a compound of those four.
| Published tool | The question it answers | Endpoints behind it |
|---|---|---|
search_orders | Which order is this person talking about | 3 — by email, by id, by date range |
get_order | What is actually on this order | 6 — order, items, charges, seller, delivery, policy reference |
start_return | Get the thing collected | 4 — eligibility, label, pickup booking, ticket note |
offer_replacement | Send another one | 3 — stock check, reservation, order amendment |
The row worth arguing about is the second. get_order fans out to six internal calls that the agent never sequences and never sees, and that compounding is the point rather than an optimization. The alternative — publishing fetch_charges, fetch_seller, fetch_delivery and letting the model chain them — is four round trips, four turns of context growth, and four chances for a run to stop halfway with the charges read and the seller unknown. Chapter 3's granularity argument does not soften because there is a protocol in the middle. It gets sharper, because now the wrong surface is published to everyone.
Shaping the Results at the Server
The order payload as the API returns it carries 68 fields: warehouse routing codes, tax jurisdiction ids, two internal status enums, and three timestamps that disagree with each other by rounding. The server trims it to the 14 fields support actually uses. This is not tidiness — a tool result is appended to the context and re-sent on every subsequent turn, so 54 unnecessary fields are paid for on turn three, turn seven and turn eleven of the same ticket.
The server also resolves references while it still has the domain knowledge to do it. The raw payload names the seller by id and stops there, but the rule that settles a return depends on whether the seller is Sundry or a marketplace seller and which of roughly 2,000 per-seller supplements applies. The server looks that up and returns the policy reference and the window in days alongside the order. Doing it server-side removes an entire turn from the median return ticket, which is worth about 1.2 seconds of buyer wait and one full re-send of the growing context.
Where the Seller's Product Description Enters
Every order has items, and every item has a product description. On a marketplace order that description was written by the seller. Nobody at Sundry wrote it, nobody reviewed it, and from the moment get_order returns it, it sits in the model's context word for word, alongside the system prompt and the buyer's ticket, being read by the same model that decides whether to issue a refund. Sundry lists about 2,000 third-party sellers. That is 2,000 people with a write path into the agent's context, through a system Sundry builds and operates itself.
{
"order_id": "SU-88421",
"placed": "2026-03-08", "delivered": "2026-03-10",
"seller": {"id": "SLR-2210", "name": "Ashcombe Furniture", "type": "marketplace"},
"policy": {"ref": "seller-supp/SLR-2210", "return_window_days": 14},
"charges": [
{"kind": "item", "cents": 11800},
{"kind": "delivery", "cents": 995},
{"kind": "delivery", "cents": 995} /* charged twice */
],
"items": [{
"item_id": "ITM-1",
"title": "Ashcombe 4-Shelf Unit, Oak",
"description": {
"origin": "seller", /* written by SLR-2210, not by Sundry */
"reviewed": false,
"text": "Solid oak shelving, flat-packed, 180cm, assembly ..."
}
}]
}
Read the shape rather than the values. The order id, the dates, the seller record, the resolved policy reference and the charges are Sundry's own data — and the duplicate delivery charge the canonical ticket complains about is visible right there in the array. The description is the only field that did not come from Sundry, and it is the only one not returned as a bare string. It is wrapped with its provenance: who wrote it, and whether anyone vouched for it. That wrapper costs a handful of tokens per item.
Be exact about what the marking does. It does not make the text safe, it does not stop the model reading it, and a label is not a wall — a sentence saying "this came from a seller" sits in the same context as the sentence it is labelling, read by the same reader. What it buys is that the boundary is now visible in the payload, in the trace, and in every eval fixture, so any control that wants to treat third-party content differently has something to key on rather than a heuristic guess about which substring came from where. Marking it at the server, once, is a design decision. Discovering the boundary during an incident is the other option. Chapter 12 is where that text turns hostile and where the controls that actually bound it get built.
Errors and Boundaries
Three failures are common enough to deserve defined, structured errors rather than a generic one. An order id that does not exist. An order too old for carrier tracking, because the carrier retains scan history for 90 days and this delivery was in November. A suspended seller, where a return is still possible, a replacement is not, and any attempt to message the seller will fail. Each error names the condition and, where there is one, the valid range.
Chapter 3's rule crosses the boundary unchanged: an error a model can act on says what was wrong and what would have been right. "Order not found" teaches nothing and gets retried verbatim. "No order matches SU-9941 — order ids are SU- followed by five digits, and this one has four" gets the next call right, and it costs the orders team one sentence in the server. The team writing those sentences has never watched a model retry the same malformed id three times. Show them once and the sentences improve permanently.
Ownership and Change
The orders team owns the tool descriptions and the result shape, because that is where the domain knowledge is. The agent team owns the 120-ticket eval set, because that is where a description change shows up as a number. Those two facts only work together if somebody writes down how they meet: a description or schema edit is a behaviour change, it runs against the eval set before it ships, a drop of more than two points blocks it, and the agent team's pager is the one that rings. Four lines, agreed before the first edit rather than after the first regression.
The other agreement is about who the server is for. Built around the support agent's current prompt, it fits one consumer and breaks on the second — and at Sundry the seller-operations agent arrived within a quarter. Built around what the business does with an order, the same four tools served both, because the domain outlives any particular agent's system prompt. The test is simple: if a tool's description mentions the support queue, it was designed for a consumer rather than for the domain.
- Publishing the API's own shape because it is less work — the agent then sequences four internal calls to answer one question, pays for four turns of context growth, and can stop halfway with the charges read and the seller still unknown.
- Returning seller-supplied text as a bare string alongside Sundry's own data — the context now contains content from 2,000 unreviewed authors with nothing distinguishing it, and everything in Chapter 12 starts from exactly this payload.
- Letting the server team edit descriptions without an eval run — tool selection accuracy moves the same week, the resolution number moves with it, and nobody connects the two because no code changed on the agent side.
- Designing the surface around the current agent's prompt — the second consumer arrives within a quarter, finds tools whose descriptions talk about the support queue, and copies the wrapper problem this chapter opened with.
- Derive the tool surface from transcripts of the work first, then check it against the API index — never the other way around.
- Mark third-party content explicitly in results, at the server boundary, with its origin and whether anyone reviewed it.
- Agree an eval-run requirement for description and schema changes, with a blocking threshold, before the first change happens.
- Keep result shaping and reference resolution on the server, where the domain knowledge lives and a saved lookup saves a whole turn.
Knowledge Check
The orders team suggests publishing all forty endpoints as forty tools, arguing that the model can compose whatever it needs. What is the strongest objection?
- The agent sequences your internal calls itself: more turns, more context, and more ways to stop halfway through
- Forty tools exceeds what an MCP server is permitted to publish, so the server would refuse to publish the list
- Forty endpoints put forty times the load on the order system, which its own capacity plan was never sized for
- Endpoint-shaped tools cannot be given accurate descriptions, because internal endpoints have no domain meaning at all
Why does get_order return the seller's product description wrapped with its origin rather than as a plain string?
- It makes the trust boundary visible to every downstream consumer, so a control has something to key on later
- It stops the model from reading the seller's text, so third-party content never influences the agent's decisions
- It compresses the description to a shorter form, which keeps a long seller listing from dominating the context
- It lets the protocol validate the field against a schema, which rejects any description containing instructions
The orders team wants to reword a tool description to make it clearer. Who should have to do what before that ships?
- The change runs against the agent team's 120-ticket eval set, with an agreed threshold that blocks a regression
- The agent team reviews the wording and approves it, since they are the ones who understand how the model reads it
- The agent team pins the old version and upgrades on their own schedule, which keeps the change out of production
- Nothing — the orders team owns the descriptions, so a wording change is entirely theirs to make and deploy
A return's window depends on which of roughly 2,000 seller supplements applies. Where should that lookup happen?
- On the server, returned alongside the order, because it saves a turn and the domain knowledge lives there
- In the agent, as a second tool call the model makes once it has read which seller the order belongs to
- In the system prompt, stated once as standing policy so the model always has the return rules available to it
- Preloaded as resources at connect time, so the supplements are already in context before any ticket arrives
You got correct