Skip to main content
Glama

Server Configuration

Describes the environment variables required to run the server.

NameRequiredDescriptionDefault
LLM_MODELNoOverride the agent's model.
LLM_API_KEYNoAPI key for the agent's model provider.
SOUS_DB_URLNomysql://user:pass@host:3306/db to use MariaDB with system-versioned tables.
LLM_BASE_URLNoBase URL for a local OpenAI-compatible model server.
LLM_PROVIDERNoOverride the agent's model provider: gemini, verda or local (any OpenAI-compatible server).
SOUS_DATA_DIRNoData files, catalog index, SQLite state and the Ed25519 signing key../data
SOUS_DEMO_NOWNoRuns the server on the disclosed demo clock; shown in /health as clock.mode.
SOUS_STATE_DBNoSQLite state file, used when no MariaDB URL is set.$DATA_DIR/state.db
GEMINI_API_KEYNoModel key for the LLM buyer agent (agent/) and scripts/llm_eval.py. Not used by the gateway.
SOUS_QUOTE_TTLNoQuote lifetime in seconds (shortened further by order cut-offs).900
SOUS_NTFY_TOKENNoOptional ntfy access token used with SOUS_NTFY_TOPIC.
SOUS_NTFY_TOPICNontfy topic for push notifications to the buyer's phone. Optional SOUS_NTFY_SERVER (default https://ntfy.sh) and SOUS_NTFY_TOKEN.
SOUS_VALKEY_URLNoAiven for Valkey (rediss://...): one Valio call budget, circuit breaker and offer cache shared by every deployment. Empty = per-process only.
MARIADB_PASSWORDNoUsed by compose.yaml for the MariaDB service and to build SOUS_DB_URL.
SOUS_NTFY_SERVERNoOptional ntfy server used with SOUS_NTFY_TOPIC.https://ntfy.sh
SOUS_APPROVER_CODENoIf set, human actions (approve, reject, issue or revoke a mandate) need it in the X-Approver-Code header.
VALIO_AIMO_API_KEYYesKey for Valio Aimo's price, availability and download APIs. Required. (Obtain it from the Junction challenge portal.)
VALIO_AIMO_API_BASENoValio Aimo API base URL.https://junction26.valioaimo.fi/api
VALIO_AIMO_CUSTOMERNoCustomer number sent to Valio Aimo (the mock ignores it).123
LLM_REASONING_EFFORTNoOverride the agent's model reasoning effort.
SOUS_DEPLOYMENT_ROLENoprod, staging, ci, demo or dev. Only prod may use the whole shared budget; the others stop at half of it.dev
SOUS_PUBLIC_BASE_URLNoPublic URL, used to build order_url and approval_url links.
SOUS_TRUST_CF_HEADERNo1 to take the client IP from CF-Connecting-IP (only behind Cloudflare).
SOUS_AIMO_CALLS_PER_MINNoPer-process cap on live Valio Aimo API calls per minute.60
SOUS_NOTIFY_WEBHOOK_URLNoWebhook that receives notifications as JSON. With SOUS_NOTIFY_WEBHOOK_SECRET the body is HMAC-SHA256 signed.
SOUS_RATE_LIMIT_PER_MINNoPOST requests per client IP per minute.60
SOUS_REQUIRE_AGENT_AUTHNo1 = every mandate needs the agent's bearer credential for quotes, orders and renewals, not only those with require_agent_auth.
VERDA_INFERENCE_API_KEYNoModel key for the LLM buyer agent (agent/) and scripts/llm_eval.py. Not used by the gateway.
SOUS_NOTIFY_WEBHOOK_SECRETNoSecret used to HMAC-SHA256 sign the notification webhook body.
SOUS_VALIO_GLOBAL_CALLS_PER_MINNoShared Valio Aimo calls per minute across all deployments (sliding window in Valkey).60

Instructions

Guidance the server publishes about itself, which clients place ahead of the tool catalog so the model reads it before choosing anything.

You buy from Valio Aimo (Finnish food-service wholesaler) for a buyer, under the mandate that buyer signed. The gateway's rules decide what you may order alone; you can't override them.

Core tools, in this order: my_mandates -> search_products -> request_quote (or can_i_order) -> place_order. Every other tool is optional.

Worked example. The user (the hotel) says: "1,000 litres of lactose-free milk for Monday."

  1. my_mandates() -> pick the hotel's mandate, e.g. "M-HOTEL-AURORA". Never ask the user for ids it gives you.

  2. search_products(query="milk", claims=["FREE_FROM_LACTOSE"]) -> several plain lactose-free milk ids. Dietary needs go in the filters (claims, avoid_allergens), not only the text. Free; no prices here.

  3. can_i_order(mandate_id="M-HOTEL-AURORA", delivery_date=<Monday, YYYY-MM-DD>, items=[{product_ids: [the ids], amount: 1000, unit: "L", product_type: "milk"}]) -> answer "yes" (order it), "needs_a_human" (ordering sends it to the buyer's human) or "no" (say why, with the rule), plus the quote_id. request_quote returns the same quote in full: read its mandate_preview, uncovered and options before ordering. If options are offered, show them or take one with request_quote(option_id=...); never drop a shortfall silently.

  4. place_order(mandate_id="M-HOTEL-AURORA", quote_id=<quote_id>, idempotency_key=<unique, reused on retry>) -> ordered | pending_approval (give the user approval_url) | refused (tell them which rule).

Ask for amounts in L or KG with several candidate ids per need; mark substitutes with substitute_for (suggest_substitutes finds them). Send your credential as "Authorization: Bearer " and never print it. An ordered order is placed with Sous; when erp.simulated is true, say the Valio hand-off is simulated in this demo. On an error follow its "hint". Never state a price, stock level, date or allergen fact a tool didn't return: say "unknown". Missing allergen data is not safe.

Capabilities

Features and capabilities supported by this server

Protocol revision2025-11-25

CapabilityDetails
tools
{
  "listChanged": false
}
prompts
{
  "listChanged": false
}
resources
{
  "subscribe": false,
  "listChanged": false
}

Tools

Functions exposed to the LLM to take actions

NameDescription
search_productsA

Search Valio Aimo's catalog (17.5k products) by name, brand or ingredient in Finnish, Swedish or English.

claims: required nutritional claims, e.g. FREE_FROM_LACTOSE, FREE_FROM_GLUTEN, VEGAN. avoid_allergens: GS1 codes to exclude, e.g. AM (milk), AN (tree nuts), AP (peanuts), AW (gluten), AE (egg). Group codes cover the family (AN = all tree nuts). Products that contain them are dropped; products with no allergen data are kept but marked 'unknown'. Full list: resource sous://allergens. labels: e.g. GOODS_FROM_FINLAND_BLUE_SWAN, EU_ORGANIC_FARMING, MARINE_STEWARDSHIP_COUNCIL_LABEL. temperature: ambient, chilled or frozen. food_only: default ranks food first (equipment first only when the query names it, e.g. "milk jug"); true drops non-food (tableware, cleaning, disposables); false = no preference. origin: keep only products declared from these countries or groups, e.g. ["fi"], ["EU"], ["NORDIC"]; the country is supplier-declared (not verified) and products without one drop out. Every card and quote line carries country_of_origin. Synonyms work across languages: "butter" also finds voi / smör. Returns {products: [card], note}: cards without price or stock; pass several of the ids to request_quote next.

get_productA

Full static details of one product: names, pack sizes, allergens, claims, labels, reliability, CO2 estimate.

No price or stock: those come only from request_quote.

request_quoteA

Get a signed, expiring quote with live prices and availability from Valio Aimo. Places no order.

delivery_date: YYYY-MM-DD. items: one per need: {product_ids: [...], amount, unit: 'L' | 'KG' | 'SALES_UNIT', substitute_for?, use?} (use: what the product is for, e.g. 'coffee', 'dessert', 'ingredient-in-recipe:Tiramisu'; a substitute that doesn't suit it, like another brand of whipping cream for a dessert, waits for a person) (pack, packs mean SALES_UNIT = cartons/packs as sold; 'ST' (PCS, KPL) counts pieces when the product has them: 20 ST of a carton of 6 = 4 cartons, rounded up, and the line says so in unit_note). Give several candidate ids per need and an amount in L or KG: the gateway picks the cheapest orderable one per L/KG, splitting across products when stock is short. Mark substitutes with substitute_for. Always pass mandate_id: the quote then includes mandate_preview (silent | notify | ask | block, and why). Check quote.lines[].orderable, quote.uncovered and mandate_preview, then place_order with quote.id before quote.expires_at. If quote.options is present, an item can't be covered as asked (stock short, later date, cut-off passed, undeliverable). Each option is a complete alternative for that item (kind, lines, total, delivery_dates, tradeoff, substitute, mandate_preview), best first. Present them to the buyer, or pick per the mandate (an option whose mandate_preview is silent/notify needs no human). Never silently drop the uncovered amount. option_id: to take an option, call request_quote again with only agent_id, mandate_id and option_id (e.g. 'Q-1A2B3C4D.O2'): you get a fresh signed quote with that item's lines replaced; then place_order it. rationale: one sentence on the buyer's business need (e.g. "breakfast milk for 400 guests"); it is signed into the quote and shown to the humans who audit or approve. State the need, not your reasoning steps. declared_risks: optional, up to 5 short sentences (200 chars each) on what YOU are unsure about (e.g. "not sure 10 L covers Saturday"). Each one holds the order for a human (rule agent.declared_risk) with your words; it can never make an order go through more easily. Say it here rather than ordering on a guess. buyer_request: the buyer's own message to you, verbatim (at most 2,000 characters), not your summary. Signed into the quote; the gateway reads its quantities ("1 539 litraa" = 1,539 L) and, when the quote has none of them, asks a person (rule quantity.request_mismatch). mandate_preview.request_quantities shows what it read. offer_id: a Valio Aimo offer from list_offers (e.g. 'OF-1A2B3C4D'), only when the buyer wants it: name the offer's product_ids in items; they are priced at min(offer price, live price) and the quote carries offer. The mandate decides as always (rule offer.applied tells the buyer). agent_id: optional when your credential covers mandate_id (see my_mandates); required without a mandate. agent_token: only if you can't send the Authorization: Bearer header and the mandate requires agent auth. Returns {quote (schema /schemas/quote.json), jws (sous-quote+jws over quote)}.

can_i_orderA

What am I allowed to do? One step: quotes the request live under the mandate (same as request_quote) and answers yes / needs_a_human / no, with the rules that decided, the lines it would order and the quote_id. Places no order. If the answer is yes or needs_a_human, call place_order with quote_id (and an idempotency_key) before expires_at; if no, tell the user why. items as in request_quote (amount in L or KG, several ids).

suggest_substitutesA

Good / better / best substitutes for a product that can't be ordered (or for any product).

purpose: what the buyer uses the product for ('coffee', 'recipe: lasagne', 'allergy menu'); substitution exact | ask | allowed. A product the buyer needs exactly (by purpose or the mandate's product_purposes) has no substitutes; for 'ask', any substitute needs the buyer's yes. Pass the same purpose on request_quote items.

Same category, measure and temperature class; keeps the original's dietary claims and the mandate's allergen rules (never relaxed). Each option lists its compromises in plain language and has a live offer. Tell the buyer the compromises, then request_quote for the chosen one with substitute_for=. Another kind of product is never offered: with nothing acceptable, no_safe_substitute says to ask a human and not_offered lists what was ruled out and why. delivery_date null = the next delivery day. With mandate_id it is read-scoped like get_mandate (send your Authorization: Bearer header or agent_token). use: what the product is used for (coffee, whipping, dessert, baking, cooking, drinking, melting, ingredient-in-recipe:). Whipping cream for a dessert needs the same product (another brand whips differently); for coffee any brand will do. Options then carry use_sensitivity, use_ok and use_reason.

place_orderA

Order a quote under a mandate. This is the only call that commits the buyer to a purchase.

The mandate engine decides, not you: status ordered (silent/notify), pending_approval (a human must approve at approval_url; poll get_order) or refused (decision.findings say which rule fired). Always send an idempotency_key and reuse it on retries: the same key never orders twice (the retry returns the original order); reusing a key for a different quote fails with idempotency_key_reused. Returns the order (schema /schemas/order.json): id (PO-...), status, decision {outcome, findings}, lines, totals, approval_url when pending, receipt_jws (signed receipt) and, once ordered, erp (the hand-off to Valio Aimo's order system). In this demo erp.status is "simulated" with a SIM- confirmation_id: tell the buyer the order is placed with Sous and the Valio hand-off is simulated, not that Valio confirmed it. rationale: one sentence on why the buyer needs this order; the approver sees it next to the mandate's findings. declared_risks: optional, up to 5 short doubts of your own (as in request_quote); each holds the order for a human. buyer_request: the buyer's own message, verbatim, if you didn't pass it to request_quote (checked the same way). agent_id: optional when your credential covers mandate_id (see my_mandates). agent_token: only if you can't send the Authorization: Bearer header and the mandate requires agent auth.

get_orderA

Current status of an order (ordered, pending_approval, refused, rejected, expired, expired_cutoff) and any human decision. Only orders under your own mandate: send your Authorization: Bearer header (or agent_token). Returns the order (schema /schemas/order.json) with approval {by, approved, note, at} once a human decided, a fresh receipt_jws, and agent_claim (the rationale, labelled unverified). After delivery problems it carries amendments[] from Valio (short pick, delay, substitution at pick, cancelled line; status pending_buyer waits for the buyer, ask-tier for their human, not you) and credit_notes[]. erp: the hand-off to Valio Aimo's order system; erp.simulated / status "simulated" / SIM- id means placed with Sous, Valio hand-off simulated in this demo (no real order reached Valio).

my_mandatesA

Call this first. The mandates your credential (Authorization: Bearer agent token, or the OAuth sign-in of this connector) was issued for: mandate_id, buyer, label (e.g. hotel / hospital / school), agent_id and a one-line summary of the rules. Pick the mandate by the buyer the user names ("the hotel"); pass its mandate_id to request_quote / place_order (agent_id can then be left out). active=false: revoked, expired or its sandbox closed. Empty without a credential: ask the user for the agent token or a mandate_id; never guess one.

get_mandateA

The mandate you act under (spend limits, allergen and claim rules, what needs a human), with its signature. Only your own mandate (send your Authorization: Bearer header or agent_token) or a public demo mandate. Returns {mandate (schema /schemas/mandate.json), jws (sous-mandate+jws), issuer, credential?, revocation?}.

get_suggestionsA

Valio Aimo's own COMMERCIAL suggestions for your buyer: the seller promoting products, not neutral advice.

Better-value alternatives to what the buyer usually orders (cheaper per L/KG, a certification label, more reliable delivery or a lower CO2 estimate), seasonal products for delivery_date, and recently listed ones. basis: optional product ids (e.g. the current cart) to find alternatives for, besides order history. Everything was pre-checked against the mandate (nothing it would block is shown) and has a live offer and mandate_check (the tier it would get at the usual quantity). Nothing is ordered. Present each one to the buyer as "Valio Aimo suggests...", with its reason and disclosure; never as your own recommendation, and never order one unless the buyer asks: then request_quote as how_to_accept says. Read-scoped like get_mandate: send your Authorization: Bearer header (or agent_token) unless the mandate is a public demo; without one the error is agent_auth_required.

list_offersA

Valio Aimo's active COMMERCIAL offers made to your buyer: signed (jws, typ sous-offer+jws) prices below the current one for a reason (near best-before, seasonal, a replacement for a recalled product, a new product), with a quantity cap and valid_until. Products the mandate rules out (avoided allergens, categories, claims) were never offered. Nothing is ordered: present each to the buyer as "Valio Aimo offers...", with its headline and disclosure, and only if the buyer wants it call request_quote with offer_id as how_to_accept says; the mandate decides as always (a first-time product may still need a human). Read-scoped like get_mandate: send your Authorization: Bearer header (or agent_token).

create_standing_orderB

Save the buyer's recurring weekly order (e.g. milk + butter + cream every Monday). Places no order.

items: [{product_ids: [...candidates], amount, unit: 'L' | 'KG' | 'SALES_UNIT', label: 'milk'}]. weekday: mon..sun, the usual delivery day. Renew it each week with renew_standing_order.

renew_standing_orderA

Renew this week's standing order: live quote, drift check against past renewals, then the order.

amounts: this week's amount per item, in item order, if it differs from the usual (omit for the usual). The gateway compares with the median of recent renewals: quantity creep, cost or price drift, a different product, new Terminal or reliability issues. Any drift puts the order on hold for a human (pending_approval); tell the buyer drift.summary and share order.approval_url. delivery_date defaults to the next delivery day. agent_id: optional when your credential covers the standing order's mandate (see my_mandates). agent_token: only if you can't send the Authorization: Bearer header and the mandate requires agent auth.

get_standing_orderB

A standing order with its renewals (newest first, simulated ones marked) and the drift baseline. Returns {standing_order, renewals, baseline, next_delivery_date, versions, rules}.

Prompts

Interactive templates invoked by user choice

NameDescription
purchaseWalk through search -> quote -> order for one purchasing request.

Resources

Contextual data attached and managed by the client

NameDescription
sous-cardsQuote, order, approval and substitute cards for the Sous gateway's tool results (MCP Apps).
allergensGS1 allergen codes for avoid_allergens, and the group codes that cover a whole family.
rules_resourceEvery rule the gateway can fire: tiers, owner (buyer mandate / Valio / gateway), text (and text_fi, Finnish), mandate field. Decision findings carry rule_url pointing at the same entries (GET /rules/{id}).
holds_resourceProducts Valio Aimo has on hold right now (recall, quality hold, sales stop): they can't be ordered. Search cards of held products carry `held`; quotes mark them PRODUCT_HELD (rule product.held).
lost_demand_resourceWhat buyers' agents wanted from Valio Aimo and didn't get, and why: supply problems vs buyer rules vs approval too late, uncovered quantities and why candidates were passed over. Totals only (no buyer names).
agent_ordering_resourceWhether Valio Aimo has paused agent ordering for every buyer (and why), and how many single buyers are paused. While a pause covers your buyer, every order is refused with rule ordering.paused, whose message says why; searching and quoting still work.

TDQS

A3.9/5.0

Scored across 14 tools

Disambiguation4/5

Most tools target clearly distinct stages of the procurement workflow: discovery, quoting, ordering, mandate inspection, and standing orders. The main overlap is request_quote versus can_i_order, which explicitly performs the same live quoting step but returns a permission answer; get_suggestions and list_offers also both surface commercial recommendations, though their descriptions distinguish neutral alternatives from seller offers.

Naming Consistency4/5

Nearly all tools follow a predictable snake_case verb_noun pattern such as search_products, request_quote, place_order, and create_standing_order. my_mandates is a minor deviation as a noun phrase with a possessive, and can_i_order is phrased as a question, but the conventions remain readable and mostly consistent.

Tool Count5/5

With 14 tools, the set is well-scoped for a complex food-procurement agent that must handle product search, quotes, mandates, orders, offers, and standing orders. Each tool has a clear role in the workflow, and the count stays within a reasonable range without obvious padding.

Completeness4/5

The surface covers the core lifecycle: find products, inspect details, quote, check permissions, place orders, track order status, inspect mandates, and manage standing orders. Minor gaps exist around canceling or amending orders and listing past orders, but these appear to be handled through human approval flows or Valio-side amendments rather than blocking core workflows.

Maintenance

ActivityMaintained
ResponsivenessNo recent community issues