# Custos — Agent Instructions

Operational guide for AI agents connecting to the **closed beta** MCP server.
Endpoint, OAuth metadata and the dynamic-registration endpoint measured 2026-10-07 against `custos.mcp.talentedai.se`.
The tool and outcome-code sections below were written 2026-09-24 (source revision `f5697af`) and have not
been re-verified against Custos. Where they disagree with what a tool returns, trust the tool.
Custos also has household skills (`get_skill`, `manage_skill`), not described here.

> **Independent service:** This server is operated by Talented AI and is not affiliated with or endorsed by Coop.

---

## What the server does

The server lets an AI assistant manage a household's shopping list and send it to the household's Coop
cart. The assistant can search products, build a list, adjust it, read what is in the cart, and send
the list to Coop — either directly (`commit_plan`) or via a link that a person approves
(`request_approval`).

## What the server does not do

- It cannot create a household, invite a new member, or connect a Coop account.
  **All of that happens on the web, not in MCP** (see the Onboarding section below).
- It has **no `clear_cart` tool** and no way to empty the entire cart in one call.
  A line is emptied by setting `quantity=0` in `adjust_plan_line` and then running `commit_plan`.
- It cannot search products without an open household with a selected Coop store.
- It gives no purchase instructions; it writes to the Coop cart but never goes to checkout and never
  places an order.

---

## Onboarding happens on the web

An agent **cannot** create a household or connect a Coop account via the MCP tools.
Always send a person to the web for these steps:

**Access is limited (closed beta).** A new account that signs in is told "Custos is in a closed beta" and
can apply for access, unless the person has been invited (by the board, or through a household invite link).
Sign-up and sign-in use a one-time code by email. An agent cannot
grant or request access on a person's behalf.

**Create a household** — the creator becomes Approver:
`https://custos.mcp.talentedai.se/start`

**Connect a Coop account** to the household:
`https://custos.mcp.talentedai.se/households/{id}/connect`

**Invite a new member** — one-time link, valid 24 h:
`https://custos.mcp.talentedai.se/households/{id}/invite`

**Remove a member:**
`https://custos.mcp.talentedai.se/households/{id}/members`

Roles: `Member` and `Approver` only.

For a step-by-step connection guide, see
[/guider/koppla-custos-till-din-ai-assistent](https://talentedai.se/guider/koppla-custos-till-din-ai-assistent).

**Source:** `src/Households.Api/Program.cs` (onboarding routes registered outside the MCP surface)

---

## Connection and authentication

**Endpoint:** `https://custos.mcp.talentedai.se/mcp`  
**Transport:** Streamable HTTP, stateless — each call is independent of the next.  
The server sends its own instructions at `initialize`; this file complements them.

**Source:** `src/Households.Api/Program.cs:128-138` (Stateless = true), `:278` (MapMcp); server instructions: `src/Households.Api/Program.cs:150` (`ServerInstructions.Text`, Custos revision `7a5f5f5`)

### OAuth 2.1

| Parameter           | Value                                                                       |
| ------------------- | --------------------------------------------------------------------------- |
| Flow                | Authorization Code + PKCE (S256 required)                                   |
| Sign-in             | Entra External ID, one-time code via email                                  |
| Access token        | 15 minutes                                                                  |
| Refresh token       | 30 days, rotating with reuse detection                                      |
| Reuse leeway        | 5 minutes (protects against parallel refresh calls)                         |
| Metadata (RFC 9728) | `https://custos.mcp.talentedai.se/.well-known/oauth-protected-resource/mcp` |

**Scopes:**

- `household.read` — read household status and list
- `household.plan` — build and send the shopping list
- `household.connect` — manage the Coop account
- `offline_access` — refresh token

**Source:** `src/Households.Api/Auth/OpenIddictSetup.cs:114,123-126,131-137`;
`src/Households.Api/Program.cs:249`

### Client registration

This closed beta uses **dynamic client registration (DCR)**. Your MCP client registers itself
automatically when you paste in the endpoint URL — no manual `client_id` is required.

The person signs in with Entra External ID: they receive a one-time code by email and approve access
in their browser. The MCP client stores the issued tokens and renews them automatically.

> A `client_id` issued by DCR is tied to this endpoint only. A client registered against another address does not
> work here; add Custos as a new connection and the client registers again.

**Source:** Measured 2026-10-07 against `custos.mcp.talentedai.se` (`/.well-known/oauth-authorization-server` lists `registration_endpoint`)

---

## Before the first tool call — cold start

If a tool call fails with a connection, timeout, or "not connected" style error on the first attempt
after a period of inactivity, retry once before reporting anything to the person. The service starts
from idle and the first call can time out before it is up. Do not tell the person the connection is
missing until a second attempt has also failed.

---

## Tools

All household-based tools accept an optional `householdId`. Leave it empty if you belong to only one
household — the normal case. If you belong to more, call `list_households` and pass the right `Id`.

Tools in working order:

### 1. `list_households`

Lists the households you are a member of. No arguments. Returns `Id`, name, your role, and any store
code. Call this when another tool asks you to select a household.

### 2. `get_household_status`

Shows the household's status: selected store, whether a Coop account is connected (`CoopConnected`),
whether a shopping list is open, and whether sending to the cart is possible (`CanAddToCart`).
`AgentManagesWholeCart` tells you whether you may change lines that household members added themselves
(see Ownership below). `CoopConnected` is the authoritative source — a selected store does **not**
mean the account is connected.

### 3. `search_stores`

Searches Coop stores by name or city. Returns `StoreCode` for use in `select_store`.

### 4. `select_store`

Sets which Coop store the household shops at. Requires `StoreCode` from `search_stores`.
After this, `add_requirements` can find products.

### 5. `add_requirements`

Adds items to the shopping list in plain language — "porridge oats 500 g", "two litres of oat milk".
Search, filtering, and product selection happen server-side. Returns the selected option per line plus
up to three alternatives for correction. **Adds only** — `quantity` must be at least 1.

> ⚠️ **Weight goods:** the response from `add_requirements` currently lacks a unit.
> Always write the weight in the text: `"ginger 200 g"`, never `Quantity: 200`.
> Read the `Unit` field in `review_plan` **before** reporting a quantity to the person.

### 6. `review_plan`

Reads the household's current shopping list: lines, selected products, and the requirements each line
must fulfil. A line with `Quantity = 0` is a **removal** — `CartEffect` says so. Show removals to
the person together with additions before running `commit_plan`.

### 7. `adjust_plan_line`

Changes a line on the list. Two things are called "remove" and they are **not** the same:

**`quantity=0`:** The line stays on the list as a removal instruction. `commit_plan` will then empty
that cart line. This is how you remove a product from the cart.

**`remove=true`:** The line is deleted from the list entirely. The cart is not touched. Use this only
when the line was added by mistake and was never sent.

A list that has already been approved cannot be changed — the person must start a new list.

### 8. `request_approval`

Returns a short-lived link that a person can open to view the list and approve it.
Pressing the button on that page approves the list **and** sends it to the cart in one step.
**The tool approves nothing itself.** Show the link and wait — do not call it again.
Requires a connected Coop account.

> Note: `commit_plan` writes directly to the cart without a browser step.
> Use `request_approval` when the person wants to review and confirm the list themselves.

### 9. `commit_plan`

Sends the shopping list directly to the Coop cart, line by line, without a browser link.
The cart is reconciled against the list — this is **not** purely additive.
A line with `quantity ≥ 1` is added or corrected; a line with `quantity = 0` empties that cart line.
Returns the outcome per line (see Outcome codes below). Requires a connected Coop account.

### 10. `resolve_cart_conflict`

Responds to a cart conflict that `commit_plan` reported (`Outcome=Conflicted`).
Decision: `"overwrite"` (write the plan's quantity) or `"keep"` (leave the cart unchanged).
`observedQuantity` must be exactly the `InCart` value that `commit_plan` showed.
**Never call this without the person having chosen** — do not decide for them.
For a weight good, `observedWeightGrams` is also required.
`overwrite` writes the quantity directly — if someone changes the line in the Coop app while the call
is in flight, that change is replaced. The response says this every time — forward the response
message verbatim, do not summarise as "done".

### 11. `read_cart`

Reads the Coop cart live, exactly as it looks right now. Each call makes a new request to Coop —
nothing is cached. `ReadAt` shows when Coop responded.
Use this for "what is in the cart?", not `review_plan` (which shows the list) or `get_commit_result`
(which shows the last commit result and may be stale).
Outcome `Read` with empty lines = the cart is genuinely empty.
`NotConnected`, `SessionExpired`, `ProviderUnavailable` = the cart could **not** be read.
**Never** report one of the three as "empty cart".

### 12. `get_commit_result`

Shows what happened the last time a list was sent to the cart, line by line.
Some lines may succeed while others do not — a partial result is normal, not an error.
A removal is reported like any other line: `Wanted 0`, `Outcome Applied` when the cart line is gone.

---

## Outcome codes

### `commit_plan` — per line

| Outcome          | What it means                                      | What the agent does                             |
| ---------------- | -------------------------------------------------- | ----------------------------------------------- |
| `Applied`        | Written to the cart.                               | Report outcome.                                 |
| `AlreadyCorrect` | Cart already matched.                              | Report outcome.                                 |
| `Unavailable`    | Product not available in the store.                | Inform; ask about alternatives.                 |
| `Restricted`     | Technical restriction prevents sending.            | Inform the person.                              |
| `Conflicted`     | Cart line changed after the last commit.           | Call `resolve_cart_conflict` with their choice. |
| `NotOwned`       | Added by a member; `AgentManagesWholeCart` is off. | Do not touch; do not retry. Inform the person.  |
| `Failed`         | Technical error on this line.                      | Inform; consider retrying the list.             |
| `Blocked`        | Blocked by a business rule.                        | Inform; investigate household status.           |

> ⚠️ `NotOwned` and `Conflicted` are **not** errors — they are boundaries. An agent that interprets
> them as errors and retries causes harm.

**Source:** `src/Households.Domain/CartLedger.cs:154` (IntentOutcome enum), verified `f5697af`

### `read_cart` — composite status

| Outcome               | What it means                                                                                             |
| --------------------- | --------------------------------------------------------------------------------------------------------- |
| `Read`                | Succeeded. Empty lines = the cart is genuinely empty.                                                     |
| `NotConnected`        | No Coop account connected. Send the person to `https://custos.mcp.talentedai.se/households/{id}/connect`. |
| `SessionExpired`      | The Coop session has expired. The household needs to reconnect the account on the web.                    |
| `ProviderUnavailable` | Coop is not responding right now. Try again later.                                                        |

**Source:** `src/Households.Application/Ports/GroceryPorts.cs:261` (CartReadStatus enum), verified
`f5697af`

---

## Cart ownership and AgentManagesWholeCart

Behaviour depends on the household's setting (`Household.AgentManagesWholeCartEffective`):

**`AgentManagesWholeCart = true`** (default for single-member households):  
`commit_plan` may change and remove lines that a household member added themselves.
The per-line result says whether this happened.

**`AgentManagesWholeCart = false`** (default for multi-member households):  
Lines the assistant did not add (`NotOwned`) are left unchanged.
Lines that _someone else_ changed after the assistant last wrote (`Conflicted`) are always refused,
regardless of setting.

Read `AgentManagesWholeCart` from `get_household_status` — do not infer it from how a previous commit
went.

**There is no `clear_cart` in the system, at any layer.**

**Source:** `src/Households.Api/Mcp/HouseholdTools.cs:39-60`, `src/Households.Api/Program.cs`,
verified `f5697af`

---

## Weight goods — mandatory rule

Always write the weight in the product text: `"ginger 200 g"`, `"minced beef 500 g"`.
**Never** specify the weight as `Quantity: 200` only — the server interprets quantity as number of
units, not as grams.

The response from `add_requirements` currently lacks a unit.
Always read the `Unit` field in the `review_plan` response **before** reporting a quantity to the
person.

---

## Approval model

`commit_plan` writes directly to the Coop cart without a browser step or a mandatory human approval
step. No part of this server requires a human to confirm before a write reaches the cart.

What protects against unintended writes:

- **`Conflicted`** — each cart line carries a content hash. If someone changed a line in the cart
  after the plan was last written, the hash no longer matches and `commit_plan` refuses that line
  with `Outcome=Conflicted`. The agent must call `resolve_cart_conflict` with the person's explicit
  choice before the line is written.
- **`NotOwned`** — if `AgentManagesWholeCart` is off and a line was added by a household member
  rather than the agent, `commit_plan` leaves it unchanged and returns `Outcome=NotOwned`. The
  agent must not retry.
- **No `clear_cart`** — the server has no tool and no internal path that empties the entire cart in
  one call. Removal is always per-line via `quantity=0`.

Use `request_approval` when the person wants to review the list in their browser and confirm before
anything is sent to the cart.

A per-line outcome report is always returned by `commit_plan` and `get_commit_result`. Show the full
report to the person — do not summarise as just "done".

---

## Stored data and contact

The server stores: users (email, name), households, memberships, lists, approvals, commit history,
cart ledger, and Coop login encrypted in Azure Key Vault. When a Coop account is disconnected the
encryption key is deleted (crypto-shredding). Logs carry only an internal pseudonymous user id.

The privacy policy is at [talentedai.se/custos/integritetspolicy](https://talentedai.se/custos/integritetspolicy).
There is no self-service account or household deletion. Contact Talented AI for these matters.

**Contact:** [talentedai.se/kontakt](https://talentedai.se/kontakt)

---

## Existing user documentation

A step-by-step connection guide for Claude and ChatGPT is at
[/guider/koppla-custos-till-din-ai-assistent](https://talentedai.se/guider/koppla-custos-till-din-ai-assistent).

---

_Endpoint and OAuth metadata measured 2026-10-07 against `custos.mcp.talentedai.se`. Tool and outcome sections: source revision `f5697af`, 2026-09-24, not re-verified against Custos._
