Skip to main content
Glama

Sous

Sous is Valio Aimo's front door for AI purchasing agents. A hotel's, hospital's or school's agent searches the catalog over MCP or REST, gets a quote signed by the gateway with live Valio price and availability, and orders under a mandate its buyer set. The customer stays in control, and autonomy is earned: plain code puts every order in one of four tiers (auto, auto + FYI, needs approval, refused), no language model ever approves an order or relaxes a rule, and the agent gets more freedom only when the buyer accepts it.

Built at Junction 2026 for Valio Aimo's challenge "Trusted Trade Agents".

The flow, in Valio's four steps:

  1. Request. The agent states what the buyer needs (amount, unit, date, what it is for) under the buyer's signed mandate (valtakirja).

  2. Lookup. Search over 17,546 products (fi/sv/en) on a local index, no API calls; live price and availability for a shortlist only, at most 2 batched Valio calls per quote.

  3. Order. A signed quote (pack sizes, VAT, cut-offs, stock as ample/limited/short) is ordered once, at that price; the mandate engine decides the tier and names the rule.

  4. Approval. A held order reaches the buyer's phone with an approve-by time (earliest cut-off minus 15 min); routine orders go through and are logged.

What keeps the buyer in control:

  • Only an authenticated person of the buyer approves. Held orders are decided on the buyer side with the approver code or a passkey; agents get 403 (audited), the claude.ai connector refuses an approver token, and Valio staff see held orders read-only.

  • No agent can become its own approver. Passkeys are enrolled only with the buyer's own credential, never by whoever opens a link first; a passkey approval signs this order at this total, including any "remember this" choice.

  • Autonomy is earned and can be taken back. After 5+ unchanged approvals over 2+ weeks for one rule and product type, Sous proposes ask → notify with the evidence; the buyer accepts it as a re-signed mandate, and a later rejection proposes the revert. Never allergens or spend limits (PROTOCOL 7.2).

  • The right product for its use. Skimmed milk for a dessert or coffee cream for whipping is held with a fitting alternative; a substitute is judged by use (another brand of whipping cream for a dessert asks, coffee cream for coffee doesn't).

  • Needs change mid-contract. A ward goes nut-free: standing, held and upcoming orders and remembered approvals are re-checked and conflicts held.

  • Buyer controls. Four-eyes (the requester can't approve), a cap on a substitute's price premium, and doubts the agent declares always reach a human (PROTOCOL 7.3).

  • A record both sides can check. Every rule at /rules (en/fi), signed quotes and receipts (Ed25519 JWS; the order card inside Claude checks the signature itself, see /ui/cards.html), a hash-chained audit log plus MariaDB system versioning: what the mandate said when each order was decided.

Try it: watch agents order live at sous.saarikko.dev · try it in the browser in 5 minutes, nothing to install or paste: /ui/try.html (one click makes your sandbox; the agent orders, you approve, you check the log; your own Claude connects from the same page) · the same by curl: docs/pitch/try-it.md · approve as the buyer: /ui/buyer.html · try six attacks: /ui/break.html · in claude.ai add the connector https://sous.saarikko.dev/connect/mcp and press "Create a demo sandbox and connect" (on staging now, https://staging.sous.saarikko.dev/connect/mcp, until production is redeployed; you get the approver token, the assistant only the agent token; other clients: docs/CONNECT.md) · honest numbers, held-out set included: docs/STATUS.md.

Download a verification bundle from an order in the reference buyer, then verify it with independently trusted public keys (Node 22+, no dependencies):

node scripts/verify_buyer_bundle.mjs order-proof.json trusted-jwks.json
node scripts/verify_buyer_bundle.mjs --tamper order-proof.json trusted-jwks.json

The second command demonstrates rejection of an altered receipt and preserves the saved proof.

Judge path in 5 minutes

The demo server is production, https://sous.saarikko.dev, set as SOUS below (the earlier 135-181-8-209.sslip.io and staging.135-181-63-134.sslip.io names still work). It is redeployed from main until the Sun 15.11 08:00 Helsinki freeze. Staging, https://staging.sous.saarikko.dev, is the fallback if production is ever down and usually runs newer code (on Sat 14.11 14:11Z /health showed prod at cdb7670, staging at ede8518); docs/STATUS.md says what runs where. Needs curl and jq.

Dates: Valio's mock gives Stock products a cut-off of Sun 15.11 17:00 for Monday delivery. After that the quickstart's Monday date is refused (availability.cutoff_passed) unless the server runs the disclosed demo clock (SOUS_DEMO_NOW, PROTOCOL.md section 21; shown in /health as clock.mode) or you ask for a later date.

  1. Get a private sandbox: copies of the hotel, hospital and school mandates, valid 24 h, 15 live Valio lookups. The two tokens in the response are shown once.

    export SOUS=https://sous.saarikko.dev
    curl -s -X POST $SOUS/sandbox | tee sandbox.json | jq '{sandbox, mandates: [.mandates[] | {mandate_id, rules}]}'
  2. Run the quickstart: the response's quickstart.curl_quote gets a signed quote for 100 L of lactose-free milk for the hotel, and quickstart.curl_order orders it.

    Q=$(jq -r .quickstart.curl_quote sandbox.json | sh | jq -r .quote.id)
    jq -r .quickstart.curl_order sandbox.json | sed "s/<quote.id>/$Q/g" | sh | jq 'if .error then . else {id, status, tier: .decision.outcome, rules: [.decision.findings[].rule]} end'

    Within the hotel's limits it is ordered, tier silent (auto), rule mandate.ok. Every finding carries a rule_url. The idempotency key is your sandbox id plus the quote id: run the order line again and you get the same order back, not a second one. Keys are scoped per agent, so no two judges clash.

  3. Approve a held order as the human: every new sandbox is seeded with a hotel standing order (four weeks of simulated history, milk creeping up) and this week's renewal already held for approval by the drift rules (standing.quantity_creep, standing.quantity_trend, standing.cost_drift); seeded in the response names it. It is priced from reference prices stored in the gateway, not a live Valio quote, and says so on the order; creating the sandbox makes no Valio call. Open the seller inbox, paste approver_token when it asks for the approver code, and approve or reject. The token works only on your sandbox's orders.

    jq '.seeded | {held_order_id, why, reasons}' sandbox.json
    jq -r .quickstart.seller_ui sandbox.json     # $SOUS/ui/?sandbox=SBX-....#/inbox
  4. Check the rules and the audit chain:

    curl -s $SOUS/rules | jq '.tiers, .count'                 # every rule: id, owner, tiers, plain text
    curl -s $SOUS/audit/verify | jq                           # recomputes the hash chain: "ok": true, events, head
    curl -s "$SOUS/audit?sandbox=$(jq -r .sandbox.id sandbox.json)&limit=5" \
      -H "Authorization: Bearer $(jq -r .agent_token sandbox.json)" | jq '.events[] | {seq, event, hash, prev_hash}'
  5. Connect an MCP client to $SOUS/mcp with the agent token, then paste one of the four prompts. Prices and stock are live, so products and totals vary; each prompt's expect says which tier and rule to look for: hotel milk (usually ordered with an FYI, spend.notify), hospital substitute (held for a human, substitution), past the cut-off (refused after the cut-off, availability.cutoff_passed), school over budget (refused above €1,500, spend.hard_limit).

    jq -r .quickstart.claude_code sandbox.json | sh   # claude mcp add --transport http sous $SOUS/mcp --header "Authorization: Bearer <agent_token>"
    jq -r '.prompts[] | .title, .prompt, ""' sandbox.json

    From claude.ai, Claude Desktop or ChatGPT instead: add a custom connector with URL $SOUS/connect/mcp, press Connect and paste the agent_token on the Sous sign-in page (OAuth; never the approver token). Cursor, VS Code and the details: docs/CONNECT.md.

Evaluation, as docs/STATUS.md states it. Held-out (LLM_EVAL.md, CI 43620135327): 15 buyer requests written blind before reading the code, Gemini 4 Hydrogen + Sous agent, 3 runs each: 0 false auto-approvals, 9/9 unsafe requests stopped (all by the gateway, the agent placed every one), but only 3/36 should-order runs matched the need (one item per request, headcounts read as pack counts). After the fixes, held-out set 2 (LLM_EVAL.md, CI 43628767992, 20 new blind requests x 3 runs, run once): again 0 false auto-approvals, 12/12 should-hold held or refused; wrong dates 12 -> 0, expected lines at the right amount 10/96 -> 73/167, dishes cookable 0/9 -> 5/9; whole requests matched with no correction still 3/48 (stock on hand read as an order, long Finnish lists merged, counts as cartons), but 27 of those runs waited for a human instead of shipping a guess and 3 went through alone (a starch as thickener, findings). The rest are our own cases. From one full CI run on CRACI, 43587725624 on f31967b: scripted cases (no LLM, live Valio prices) 60/60, 43/43 unsafe actions blocked or escalated; LLM buyer agent on 48 one-sentence cases: Kimi K3.1 Pro (Verda Inference) 47/48, Gemini 4 Hydrogen 46/48, Nemotron 3.5 Super VL 30/48, with 0 mandate violations auto-ordered by any of them (unsafe requests blocked or escalated 34/34, 34/34, 33/34); mandate drafting by Gemini 23/23. Conformance kit 17/17 on staging (CONFORMANCE.md); check-the-checker catches 47/47 planted violations (EVAL_INTEGRITY.md); Monday rush, 60 agents on 400 units of stock: 0 over-allocated (ROBUSTNESS.md). Recipe planner, 7 menus (CI 43595658113, after the R05 crash fix): recipe table 7/7, Gemini 5/7, Kimi 5/7, every miss a quantity outside our 35 % bar. Red team on a Verda GPU (REDTEAM.md, 01480fd): 2,000 generated requests through a self-hosted Qwen3-30B-A3B (vLLM), 976/997 unsafe held or refused; it found 3 order-slicing misses in the gateway (demo mandates had the split window off), fixed with a 60-minute split window (ad366ce; Gemma 4 re-run on 100 slicing requests: 0 gateway violations). Spend so far: $56.01 known (COSTS.md).

Related MCP server: AchPay

Valio's brief → where it is in Sous

Valio's brief

In Sous

Number or link

Flow Request → Lookup → Order → Approval

POST /sandbox quickstart above: search → POST /quotes → POST /orders → /ui/approve.html

CI eval: 60/60 scripted cases end to end (STATUS)

1. Lookup: structured search of products, prices, availability

GET /products / MCP search_products (local index, 0 API calls); live price + availability only on the quote

17,546 products fi/sv/en; avg 0.47 Valio calls per request, max 2 (CI 43587725624)

2. Request and order at a fixed price

POST /quotes returns an Ed25519-signed quote with an expiry; POST /orders accepts only that signed payload, once

20 parallel orders on one quote → 1 ordered, 19 rejected

3. Permissions and approval: agent alone vs human sign-off

Signed mandate → tiers silent / notify / ask / block, every rule at GET /rules; optional buyer controls: a cap on a substitute's price premium over the product it replaces, the agent's own declared doubts always reach a human, and four-eyes (separate_approver: the requester can't approve; PROTOCOL 7.3); the buyer's humans approve on the buyer side (/ui/buyer.html, or the passkey page /ui/approve.html); Valio's seller side (/ui/) sees held orders read-only, agents get 403; earned autonomy: after a clean approval record per rule and product type (≥5 unchanged approvals over ≥2 weeks, none edited or rejected) Sous proposes ask → notify, the buyer accepts it as a re-signed mandate, and a later rejection proposes taking it back (GET /mandates/{id}/autonomy, PROTOCOL 7.2)

43/43 unsafe blocked or escalated; red team 976/997 (REDTEAM.md)

Bonus: more than price

Quote lines carry delivery reliability, labels/certifications, CO₂ estimate (range across options), origin

low reliability (< 90 %) is flagged on the line

Bonus: exceptions

Partial availability → signed options (part now + rest later, later date, substitute), type-aware substitutes, use-aware substitution (a substitute is judged for what the product is used for: another brand of whipping cream for a dessert asks a person, coffee cream for coffee doesn't; rule substitution.use_sensitive), post-order amendments

eval cases O01–O06; PROTOCOL 5.1, 5.1.1; tests/test_use_sensitivity.py

Bonus: exceptions, the buyer's needs change

A ward goes nut-free mid-contract: the mandate update re-checks standing orders, held and upcoming orders and remembered approvals; conflicts are held with a reason (needs.changed), unsafe remembered approvals revoked, a safe substitute proposed, one fi/en summary; loosening expands nothing

tests/test_needs.py; PROTOCOL 8.1

Bonus: explain decisions

Every finding has a rule id, plain-language reason and rule_url; hash-chained audit at GET /audit (/audit/verify); reasons also in Finnish (/rules?lang=fi)

/audit/verify ok on prod at cdb7670 (365 events, 13:09Z)

Good to know: not a chatbot; query APIs only when needed

MCP/OpenAPI for any agent; Valio is called only for a quote's shortlist, cached, with a shared budget (Aiven Valkey)

0.47 calls/request; never a full-catalog fetch

Good to know: not production-ready in 48 h

Demo-grade identity, list prices, ERP hand-off stub labelled simulated

Known limits

Where to read more

  • docs/pitch/try-it.md: the sandbox walk-through as a one-page handout.

  • docs/STATUS.md: what works, what is deployed where, evaluation with run ids, assumptions, open questions.

  • docs/PROTOCOL.md: the full protocol. Start with 6. Tiers and rules, 12. Audit log and 18. Judge sandbox.

  • docs/DECISIONS.md: technical choices and the path to production.

  • docs/INTEGRATION.md: how Sous plugs into Valio Aimo's systems: orders to SAP (ORDERS05 outbox, stub today), customer accounts (credit limit, payment block, delivery days), contract prices, routes, a phased plan, and what we don't know.

  • docs/CONFORMANCE.md: one command (scripts/conformance.py --base <url>) runs 17 black-box checks (signatures, tampering, idempotency, self-approval, private reads, hard limit, prompt injection, audit, stock exposure) against any Sous gateway; 17/17 on staging, run output included.

  • docs/STANDARDS.md: mapping to AP2, ACP, A2A, MCP server cards, schema.org, W3C VC 2.0, JOSE, and Peppol BIS Ordering 3.0 (UBL Order and Order Response per order).

  • docs/EVAL_INTEGRITY.md: checking the checker, 47 planted violations the evals and verifiers must catch.

  • docs/ROBUSTNESS.md: the Monday rush, 60 agents on one short product, stock reservations and fair share.

  • docs/CAPACITY.md: measured capacity of one gateway process (4 CPUs, MariaDB, Valio replayed): ~23 req/s, ~3 orders/s; the bottleneck and the scale-out plan.

  • docs/CRACI.md: CI on CRACI runners, deny-by-default egress per job and the signed egress evidence.

  • docs/COSTS.md: every paid model run and running service.

  • docs/LLM_EVAL.md (per-model LLM eval, case by case), docs/CI.md, docs/VERDA.md, docs/GEMINI.md, docs/NEMOTRON.md, docs/AIVEN.md, docs/demo-runbook.md.

  • The rest of this README: features, API table, local setup, configuration, architecture.

What it does

  • Search 17,546 products in Finnish, Swedish and English, with filters for claims, allergens, labels, temperature and country of origin. Search uses a local index and costs no Valio API calls. Type-aware ranking (9eb15ec): 'laktoositon maito' ranks plain lactose-free milk above coffee milk; precision@3 90.5 % → 99.3 % on our own 49 tuning queries.

  • Signed quotes with live price and availability for a shortlist only: 2 batched Valio API calls per uncached quote, 2 more when an item is short and substitutes are priced for the options (1.0 on average with the cache in evals/results.json). Pack and lot sizes, VAT, cut-offs, stock vs Terminal, delivery reliability, CO₂ estimate, splits when stock is short.

  • Mandate engine in plain code. Every order lands in one tier: silent, notify, ask or block, with the rule that fired.

  • What always needs a human in the demo mandates: a substitute for what was asked, a product the buyer has never bought before, a unit price more than 20 % off the last price paid, and anything past the daily or weekly budget or that several small orders add up to past the auto-approve limit. Valio Aimo can pause all agent ordering (every buyer or one buyer) with one call; nothing is ordered or approved until it resumes.

  • Autonomy levels, capped by Valio Aimo: a mandate lets its agent advise only, reorder the buyer's usual products, or order; Valio can cap any buyer (or all) lower with one call, and held orders name Valio's reason.

  • Order cards in the chat (MCP Apps): in Claude (web, desktop, phone) every quote, order, substitute and renewal arrives as a card under the tool call, served by the gateway (ui://sous/card.html): lines, totals, the mandate's decision rule by rule, the approval link for the buyer's person, and the gateway's Ed25519 signature checked inside the chat. Without Claude: /ui/cards.html draws real demo orders with the same card.

  • Lost demand, Valio's view (/stats/lost-demand, stats page): the value of orders Valio couldn't supply vs ones the buyer's rules refused or a human declined, quantities no product covered, and why products were passed over (cut-off, stock, holds).

  • Peppol BIS Ordering 3.0: every order as a UBL Order and Valio's Order Response (accepted / held / rejected) at /orders/{id}/peppol/order.xml and /order-response.xml, for buyers' ERP and e-procurement systems. Validated against the official UBL 2.1 XSD and OpenPeppol BIS schematron in CI (9e5ed51).

  • Undeclared allergens, read by the Gemini Decisions API: 30 % of food products declare no allergens. For those, gemini-decision-1 answers one yes/no per avoided allergen from the product's name and ingredients; at 80 % or more the order is refused (allergen.likely). Below that it stays unknown and a human decides: a model answer never makes a product safe. Accuracy on products whose allergens are known is on /proof.

  • Orders and approvals: one order per quote, idempotency keys, cut-offs re-checked at order and at approval, signed receipts. A human approves or rejects from a phone link before an approve-by time: the earliest cut-off minus 15 minutes, capped by the mandate's and sandbox's expiry (497be36). Explanations and rule texts also in Finnish (/rules?lang=fi, 59fcce3). With no real order API, the ERP hand-off is a stub and labelled simulated (3509492).

  • Substitutes good / better / best that understand product type (sour milk is not milk), keep claims and allergen rules, and list each compromise.

  • Standing orders with drift detection: a weekly order is renewed live and compared with its last 4 renewals. Quantity creep, cost or price rises, a switched product or a new supply risk lift the renewal to ask.

  • CO₂ as a range: each quote line and the quote total show an estimate plus the low and high among the options the agent shortlisted, so the effect of the choice is visible (a spread between options, not a measurement uncertainty).

  • Purpose per item: say what a product is for (coffee, recipe: lasagne, allergy menu), per order item or once in the mandate. Coffee cream may be any brand; a substitute in a recipe needs the buyer's yes; for an allergy recipe only the exact product goes through, and Valio Aimo's suggestions, offers and substitutions at pick follow the same rule. See PROTOCOL.md section 32.

  • Origin check: every quote line and card shows the supplier-declared country of origin, a quote summarises where the goods come from, search takes origin=, and a mandate can allow or avoid countries and groups (EU, NORDIC ...). Unknown origin is never treated as allowed. See PROTOCOL.md section 30.

  • Valio suggests: Valio Aimo's own sponsored suggestions (cheaper, certified, more reliable, lower CO₂, seasonal, new), pre-checked against the buyer's mandate. Offers only, never orders, and audited.

  • Notifications for notify and ask orders and human decisions: in-app feed, ntfy push to a phone, signed webhook.

  • Audit: a hash-chained log both sides read, plus MariaDB system-versioned tables (the mandate as of each decision).

  • LLM buyer agent (agent/): any OpenAI-compatible model (Gemini, Verda Inference, self-hosted vLLM) turns a sentence into the gateway's MCP tool calls. The runtime binds identity, mandate and idempotency; the model never decides. Scored per model by scripts/llm_eval.py.

  • Snap a photo of the order (CLI; /agent/ runs on staging only, and photo upload needs an NVIDIA key there): NVIDIA Nemotron 3.5 Super VL reads a handwritten order list, a menu or a delivery note (Finnish, Swedish, English); the agent turns it into one basket quote under the mandate, and low-confidence lines go to a human instead of the basket. See docs/NEMOTRON.md.

  • Response options when an item can't be covered as asked (part now, rest later, a substitute...), each with a mandate preview. Rule register at GET /rules.

  • Evaluation (our own cases, live Valio prices; details and run ids in docs/STATUS.md), all from CI run 43587725624 (f31967b) on CRACI: scripted, no LLM: 60/60 cases, 43/43 unsafe actions blocked or escalated (incl. partial-availability options and attack cases: slicing, self-approval, prompt injection, unusual quantity). LLM agent, 48 one-sentence cases, all three models in the same run: Kimi K3.1 Pro 47/48, Gemini 4 Hydrogen 46/48, Nemotron 3.5 Super VL 30/48; 0 mandate violations auto-ordered with any of them. The miss all three share, S04, is a case whose premise (only a coconut cream without allergen data exists) holds only for the scripted run.

Try it yourself

The demo server ($SOUS, see Judge path; production is a Verda CPU instance in Finland) and staging both serve /docs, /mcp (also /mcp/), /ui/ and /buyer/; human actions need the approver code. Get a private sandbox there (copies of the hotel, hospital and school mandates, valid 24 h, kept out of the curated demo data):

curl -s -X POST $SOUS/sandbox | jq

The response has your sandbox id (e.g. SBX-7Q2K), mandate and agent ids, an agent token and an approver token (both shown once), and ready-to-paste quickstarts. Then:

  1. Connect Claude Code with the quickstart.claude_code line: claude mcp add --transport http sous <base>/mcp --header "Authorization: Bearer <agent_token>".

  2. Paste one of the four prompts (hotel milk, hospital substitute, past the cut-off, over the budget) and watch the gateway decide: ordered, ordered with an FYI, held for a human, or refused with the rule.

  3. Open quickstart.seller_ui (/ui/?sandbox=SBX-7Q2K#/inbox) and approve or reject your held orders with the approver token. It works only on your sandbox's orders.

  4. Open quickstart.break_ui (/ui/break.html?sandbox=SBX-7Q2K), paste your agent token and try six attacks (self-approval, slicing, prompt injection, a tampered quote, replay, over the limit): each shows the verdict, the rule and the audit events, with no Valio call.

Each sandbox has 15 live Valio Aimo lookups (cached offers are free); substitute lookups count toward the same budget, like quotes (and so do suggestions); GET /sandbox/<id> shows what's left. One-page handout: docs/pitch/try-it.md; details in PROTOCOL.md section 18.

API and MCP at a glance

Area

REST

MCP tool

Catalog

GET /products, GET /products/{id}, GET /allergens

search_products, get_product

Substitutes

GET /products/{id}/substitutes

suggest_substitutes

Quotes

POST /quotes, GET /quotes/{id}

request_quote

Orders

POST /orders, GET /orders, GET /orders/{id}, GET /orders/{id}/history

place_order, get_order

Approvals (human)

POST /orders/{id}/approve, POST /orders/{id}/reject

none, on purpose

Mandates

POST /mandates (human), GET /mandates[/{id}[/history]], POST /mandates/{id}/revoke (human)

get_mandate

Suggestions

GET /mandates/{id}/suggestions

get_suggestions

Standing orders

POST /standing-orders, GET /standing-orders[/{id}], POST /standing-orders/{id}/renew, .../pause, .../resume (human)

create_standing_order, renew_standing_order, get_standing_order

Notifications

GET /notifications, POST /notifications/test (human)

Audit and trust

GET /audit, GET /audit/verify, GET /.well-known/jwks.json, GET /health

Discovery and standards

GET /.well-known/agent-card.json (A2A), GET /.well-known/mcp/server-card.json (MCP), GET /schemas/{mandate,quote,order,receipt}.json, GET /mandates/{id}/ap2, GET /quotes/{id}/ap2, GET /quotes/{id}/schema-org, GET /orders/{id}/ap2, GET /orders/{id}/schema-org

Judge sandbox

POST /sandbox, GET /sandbox/{id}, DELETE /sandbox/{id} (human); ?sandbox=<id> on the list endpoints

MCP also offers the resources sous://allergens and sous://mandates/{id}, and a purchase prompt. Full details, error codes and rule ids: docs/PROTOCOL.md. How each object maps onto open standards (AP2, ACP, A2A, MCP server cards, schema.org, W3C VC 2.0, JOSE), what is exported and what is still missing: docs/STANDARDS.md.

Quick start

Needs Python 3.11+, uv and a Valio Aimo API key (from the Junction challenge portal).

uv sync
cp .env.example .env                    # set VALIO_AIMO_API_KEY
uv run python -m scripts.fetch_data     # download product, reliability and CO2 files, build the search index
uv run python -m scripts.seed           # issue the demo mandates (hotel, hospital, school, Kaarna Catering)
uv run uvicorn sous.api:app --port 8000

Then open:

Connect Claude Code as the buyer agent over MCP:

claude mcp add --transport http sous http://localhost:8000/mcp

Then ask it, for example: "You are aurora-purchasing-agent with mandate M-HOTEL-AURORA. Buy 1,000 litres of lactose-free milk for delivery on Monday." Against the demo server, use $SOUS/mcp instead.

Mandates with require_agent_auth (the seeded M-HOTEL-AURORA-SECURE, whose token the seed prints once) also need the agent's credential. Pass it as a header:

claude mcp add --transport http sous http://localhost:8000/mcp --header "Authorization: Bearer <token>"

Without it, quotes and orders on that mandate get agent_auth_required. See PROTOCOL.md, section 16.

The MCP server also runs over stdio: uv run python -m sous.mcp_server.

Demos and evaluation (each takes --base <gateway URL>; see docs/demo-runbook.md):

uv run python scripts/demo.py --list            # the video's scenes, played over REST
uv run python scripts/demo_standing.py          # standing order drifts, renewal is held (needs the approver code if set)
uv run python scripts/eval.py                   # the scripted buyer requests (60 cases), live Valio prices -> evals/results.json
uv run python -m agent.buyer "We need 1,000 litres of lactose-free milk delivered on Monday." \
    --mandate M-HOTEL-AURORA --agent aurora-purchasing-agent --gateway http://localhost:8000
uv run python scripts/llm_eval.py --provider verda   # the eval sentences through the LLM agent, per model

The LLM agent needs GEMINI_API_KEY, VERDA_INFERENCE_API_KEY or LLM_API_KEY with LLM_BASE_URL (see agent/llm.py).

From a menu to one order (recipe planner)

A chef says what they are cooking, not what to buy. agent/recipe.py turns that into one basket quote:

uv run python -m agent.recipe --mandate M-HOTEL-AURORA --agent aurora-purchasing-agent \
    "Lasagne for 120 on Monday, we have 10 kg flour and plenty of salt"            # add --order to place it

It prints the chain: recipe (dishes, portions, per-portion ingredients) -> ingredients (scaled, minus stock on hand, rounded up, in kg/L) -> products (up to 5 catalog candidates per ingredient, filtered by the mandate's allergen and claim rules) -> one signed basket quote (the gateway picks packs and the cheapest orderable product, reports what is uncovered) -> substitutes for gaps -> mandate preview -> order or ask. Gemini (when GEMINI_API_KEY is set) only reads the chef's sentence into schema-checked JSON; every number after that is code. Without a key, or with --provider table, a curated recipe table (agent/recipes.json: lasagne, lohikeitto, joulukinkku, rosolli, karjalanpaisti, kasvissosekeitto) does the reading, and the output says which path was used. agent.buyer routes menu-shaped requests here automatically. Eval: uv run python scripts/recipe_eval.py --providers table gemini (7 cases, evals/recipe_cases.yaml). See docs/GEMINI.md and PROTOCOL.md, section 20.

Tests: uv run pytest -q (offline; the MariaDB tests run when SOUS_TEST_MARIADB_URL is set).

Configuration

Read from the environment, or from .env in development. Never commit .env. The project was called Valtakirja before; the old VALTAKIRJA_* names still work as a fallback for every SOUS_* variable.

Variable

Default

Purpose

VALIO_AIMO_API_KEY

(empty)

Key for Valio Aimo's price, availability and download APIs. Required.

VALIO_AIMO_API_BASE

https://junction26.valioaimo.fi/api

Valio Aimo API base URL.

VALIO_AIMO_CUSTOMER

123

Customer number sent to Valio Aimo (the mock ignores it).

SOUS_DATA_DIR

./data

Data files, catalog index, SQLite state and the Ed25519 signing key.

SOUS_STATE_DB

$DATA_DIR/state.db

SQLite state file, used when no MariaDB URL is set.

SOUS_DB_URL

(empty)

mysql://user:pass@host:3306/db to use MariaDB with system-versioned tables.

SOUS_QUOTE_TTL

900

Quote lifetime in seconds (shortened further by order cut-offs).

SOUS_APPROVER_CODE

(empty)

If set, human actions (approve, reject, issue or revoke a mandate) need it in the X-Approver-Code header.

SOUS_REQUIRE_AGENT_AUTH

(empty)

1 = every mandate needs the agent's bearer credential for quotes, orders and renewals, not only those with require_agent_auth.

SOUS_PUBLIC_BASE_URL

(empty)

Public URL, used to build order_url and approval_url links.

SOUS_RATE_LIMIT_PER_MIN

60

POST requests per client IP per minute.

SOUS_AIMO_CALLS_PER_MIN

60

Per-process cap on live Valio Aimo API calls per minute.

SOUS_VALKEY_URL

(empty)

Aiven for Valkey (rediss://...): one Valio call budget, circuit breaker and offer cache shared by every deployment (docs/AIVEN.md). Empty = per-process only.

SOUS_DEPLOYMENT_ROLE

dev

prod, staging, ci, demo or dev. Only prod may use the whole shared budget; the others stop at half of it.

SOUS_VALIO_GLOBAL_CALLS_PER_MIN

60

Shared Valio Aimo calls per minute across all deployments (sliding window in Valkey).

MARIADB_PASSWORD

(empty)

Used by compose.yaml for the MariaDB service and to build SOUS_DB_URL.

SOUS_TRUST_CF_HEADER

(empty)

1 to take the client IP from CF-Connecting-IP (only behind Cloudflare).

SOUS_NTFY_TOPIC

(empty)

ntfy topic for push notifications to the buyer's phone. Optional SOUS_NTFY_SERVER (default https://ntfy.sh) and SOUS_NTFY_TOKEN.

SOUS_NOTIFY_WEBHOOK_URL

(empty)

Webhook that receives notifications as JSON. With SOUS_NOTIFY_WEBHOOK_SECRET the body is HMAC-SHA256 signed.

GEMINI_API_KEY, VERDA_INFERENCE_API_KEY

(empty)

Model keys for the LLM buyer agent (agent/) and scripts/llm_eval.py. Not used by the gateway.

LLM_PROVIDER, LLM_MODEL, LLM_BASE_URL, LLM_API_KEY, LLM_REASONING_EFFORT

(presets)

Override the agent's model: gemini, verda or local (any OpenAI-compatible server).

Docker

cp .env.example .env   # set VALIO_AIMO_API_KEY, MARIADB_PASSWORD (any random string), SOUS_APPROVER_CODE
docker compose up -d --build

This starts the gateway on port 8000 and MariaDB 11.4. On first start the container downloads Valio Aimo's data, builds the index and issues the demo mandates; later starts reuse the valtakirja-data volume. State lives in the mariadb-data volume.

Architecture

flowchart LR
    subgraph Buyer side
        H[Buyer human]
        A[Buyer agent<br/>Claude Code, agent/ LLM agent, scripts]
        B[Reference buyer<br/>/buyer]
    end
    subgraph Sous gateway
        MCP[MCP server<br/>/mcp]
        REST[REST + OpenAPI<br/>/docs]
        SVC[service.py<br/>search, quote, order, approve]
        CAT[(Catalog index<br/>SQLite FTS5 trigram)]
        Q[quote.py<br/>packs, lots, cut-offs, issues]
        M[mandate.py<br/>rules engine, 4 tiers]
        SO[standing.py<br/>standing orders, drift]
        SG[suggest.py<br/>Valio suggests]
        N[notify.py<br/>feed, ntfy, webhook]
        SIG[signing.py<br/>Ed25519 JWS, JWKS]
        ST[(store.py<br/>MariaDB system-versioned<br/>or SQLite)]
        UI[Valio Aimo UI<br/>/ui]
    end
    V[Valio Aimo APIs<br/>price, availability]

    A --> MCP --> SVC
    B --> REST
    A --> REST --> SVC
    H -- approve / reject --> UI --> REST
    SVC --> CAT
    SVC --> Q -- shortlist only --> V
    SVC --> M
    SO --> SVC
    SG --> M
    SVC --> N -- notify / ask --> H
    SVC --> SIG
    SVC --> ST

Repo layout

sous/             the gateway
  api.py          REST API, mounts MCP, UI and buyer
  mcp_server.py   MCP tools, resources and prompt (same operations as REST)
  service.py      search, quote, order, approve, substitutes, history
  catalog.py      static catalog and FTS5 trigram search, allergen checks
  quote.py        offers, pack and lot arithmetic, issue codes, splitting, quote expiry
  mandate.py      mandate model and the deterministic rules engine
  substitutes.py  good / better / best substitutes, product-type aware
  standing.py     standing orders and drift detection (standing_store.py: storage)
  suggest.py      Valio suggests: sponsored, mandate-checked suggestions
  notify.py       notifications: feed, ntfy push, signed webhook (notifications_store.py)
  signing.py      Ed25519 compact JWS and JWKS
  store.py        mandates, quotes, orders, hash-chained audit log (MariaDB or SQLite)
  aimo.py         Valio Aimo API client: batching, caching, call budget
  config.py       settings
agent/            LLM buyer agent: buyer.py (MCP tool loop), llm.py (OpenAI-compatible model presets)
                  recipe.py (menu -> basket), vision.py (photo -> basket, Nemotron VL)
scripts/          fetch_data.py, seed.py, demo.py, demo_standing.py, eval.py, llm_eval.py
evals/            cases.yaml (60 cases), results.json, llm_results.*.json, vision/
web/              Valio Aimo UI: approvals, orders, mandates, audit, stats, live activity
buyer/            reference buyer in the browser, verifies JWS with Web Crypto
tests/            pytest, offline plus MariaDB
docs/             PROTOCOL, DECISIONS, STATUS, CI, LLM_EVAL, VERDA, demo runbook, pitch/

License

MIT, see LICENSE. Valio Aimo's product data is not included in this repo; download it with your own API key. Without it uv run pytest -q still passes offline; the few tests and evals that need Valio data skip with the reason, and docs/PUBLISHING.md lists the commands that rebuild each file.

About this public copy

This repository is the public copy of the team's private development repository, made with scripts/publish_public.sh (docs/PUBLISHING.md). The full commit history is kept, filtered with git filter-repo: Valio Aimo's data files (product download extracts and recorded API responses) and the team's internal coordination notes are removed from every commit, and personal emails are mapped to GitHub noreply addresses. Commit hashes in the docs were rewritten to this repository's hashes. CI run ids in the docs refer to the development repository's GitHub Actions runs and are kept as plain ids; recorded results (evals/*.json) are left as recorded, so their provenance (commit, CI run) refers to the development repository.

Available Tools

14 tools
can_i_orderCore 3/4 shortcut: Can I order this?A

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).

Inputs: items (required), mandate_id (required), delivery_date (required), agent_id, +2 more. Returns: can_i_order: the mandate's answer for this request, in one step, and the quote…

create_standing_orderOptional: Create a weekly 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.

Inputs: name (required), items (required), agent_id (required), mandate_id (required), +1 more. Returns: id, name, buyer, items, +6 more

get_mandateOptional: Read the mandateA
Read-onlyIdempotent

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?}.

Inputs: mandate_id (required), agent_token. Returns: get_mandate: the signed mandate payload, its JWS (typ sous-mandate+jws), issuer…

get_orderOptional: Order statusA
Read-onlyIdempotent

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).

Inputs: order_id (required), agent_token. Returns: An order with its signed receipt. Schema: /schemas/order.json.

get_productOptional: Product detailsA
Read-onlyIdempotent

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.

Inputs: product_id (required). Returns: Full static product record (names, units, allergens, claims, labels, reliabilit…

get_standing_orderOptional: Standing order, history and drift baselineB
Read-onlyIdempotent

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}.

Inputs: standing_id (required), agent_token. Returns: rules, baseline, renewals, versions, +2 more

get_suggestionsOptional: Valio Aimo's suggestions (sponsored)A
Read-only

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.

Inputs: agent_id (required), mandate_id (required), delivery_date (required), basis, +2 more. Returns: note, buyer, agent_id, disclosure, +4 more

list_offersOptional: Valio Aimo's offers (sponsored)A
Read-onlyIdempotent

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).

Inputs: mandate_id (required), agent_token. Returns: note, buyer, offers, disclosure, +2 more

my_mandatesCore 1/4: Which mandates am I acting under?A
Read-onlyIdempotent

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.

Inputs: agent_token. Returns: my_mandates: the mandates the caller's credential covers.

place_orderCore 4/4: Place the orderA
DestructiveIdempotent

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.

Inputs: quote_id (required), mandate_id (required), agent_id, rationale, +4 more. Returns: An order with its signed receipt. Schema: /schemas/order.json.

renew_standing_orderOptional: Renew a standing order (with drift check)A
Destructive

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.

Inputs: standing_id (required), amounts, agent_id, rationale, +2 more. Returns: drift, order, renewal

request_quoteCore 3/4: Quote with live price, stock and the mandate checkA

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)}.

Inputs: items, agent_id, offer_id, option_id, +6 more. Returns: request_quote: the quote payload and its JWS (typ sous-quote+jws). Schema of `q…

search_productsCore 2/4: Search the catalogA
Read-onlyIdempotent

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.

Inputs: query (required), limit, claims, labels, +5 more. Returns: note, products

suggest_substitutesOptional: Suggest substitutesA
Read-only

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.

Inputs: amount (required), agent_id (required), product_id (required), delivery_date (required), +8 more. Returns: options, original, next_step, considered, +2 more

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    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

    Related MCP Connectors

    Related MCP Servers

    • F
      license
      Not graded
      quality
      B
      maintenance
      Enables AI agents to securely search products, obtain signed quotes, create checkouts, and track orders without directly handling prices or payment amounts, enforcing spending policies and audit trails.
      1
      -
    • A
      license
      Not graded
      quality
      C
      maintenance
      Enables AI agents to discover governed capabilities, inspect machine-readable contracts, and request policy-checked procurements with signed receipts and audit trails.
      Apache 2.0
    • A
      license
      Not graded
      quality
      B
      maintenance
      Enables AI agents to discover products, verify signed catalog feeds, negotiate within published policies, and place mandate-bound orders with signed receipts via MCP, A2A, or REST.
      1
      MIT