Skip to main content
Glama
README.md
# rami-levy-mcp

An MCP server that lets an LLM agent shop at [Rami Levy](https://www.rami-levy.co.il)
(an Israeli supermarket chain): search products, manage a cart, and reorder
from purchase history — talking to Rami Levy's real API directly.

## The one thing this package does NOT solve

Rami Levy's site is behind Cloudflare, which blocks requests that don't look
like they're coming from a real Israeli browser. This package makes the
*request*; getting that request to actually reach Rami Levy's origin without
being challenged is **your** infrastructure's job — a residential/mobile
proxy, a VPN, a box that's actually in Israel, whatever gets you there. If
`rami_levy_check_status` reports `blocked_by_cloudflare`, see the error table
below: recapture first, suspect your egress only if a fresh capture still fails.

## Requirements

Node >= 22.13. The cart store uses Node's built-in `node:sqlite` — there is
no native addon to compile. Build with:

```bash
npm ci && npm run build
```

## What you need to configure

Three required, three optional:

| Env var | What it is |
|---|---|
| `RAMI_LEVY_BEARER_TOKEN` | The `Authorization: Bearer` token from a logged-in browser session |
| `RAMI_LEVY_ECOM_TOKEN` | A separate JWT, sent as the `ecomtoken` header |
| `RAMI_LEVY_USER_AGENT` | Must match whatever browser the above were captured from |
| `RAMI_LEVY_COOKIE` *(optional)* | The full cookie string. Measured live (2026-09-18) from an Israeli residential IP: neither the orders API (`www-api`) nor search (`www`) needed a cookie at all — bearer + ecomtoken + user-agent were enough. It only helps once Cloudflare starts challenging your egress; in that case, include at least `cf_clearance`. |
| `RAMI_LEVY_STORE` *(optional)* | Store id (default `412`) |
| `RAMI_LEVY_DB_PATH` *(optional)* | Where the cart's SQLite file lives (default `./cart.db`) |

**Capturing the bundle:** log into rami-levy.co.il in a real browser, go to
`/he/dashboard/orders`, open DevTools → Network, find the request to
`www-api.rami-levy.co.il/api/v3/site/orders`, right-click it → Copy → Copy as
cURL, and pull `Authorization`, `ecomtoken`, and `User-Agent` out of the
copied headers. That single request carries everything needed — no separate
capture of the catalog search is required. Only add `RAMI_LEVY_COOKIE` if you
later see `blocked_by_cloudflare` and need to supply `cf_clearance`.

**These expire.** An expired bearer/ecom session shows up as `auth_expired`.
If you do supply a cookie with `cf_clearance`, that's a short-lived anti-bot
cookie (hours to a few days) and an expired one most likely shows up as
`blocked_by_cloudflare` (a challenge page). Either way, repeat the capture
above first.

## The 10 tools

- `rami_levy_suggest_reorder(numOrders?, minOccurrences?)` — what a reorder
  *would* add, without touching the cart. Same selection as
  `reorder_from_history`, but each candidate carries `occurrences`, `qty`
  (median) and `lastPrice`, so the user can be shown why something is
  proposed and drop what they don't want before anything is added. Read-only.
- `rami_levy_list_orders(page?)` — one page of past orders, newest first:
  `orderId`, `createdAt`, `supplyAt` (delivery slot), `status`, and `total`
  (what was charged, delivery fee included). The response carries `page`,
  `lastPage` and `totalOrders`; a real account runs to 140 orders over 24
  pages, 6 per page. Read-only.
- `rami_levy_view_order(orderId)` — one order in full: every line with
  `productId`, `name`, unit `price`, `qty` (decimal for weight-based items)
  and `lineTotal`, plus the order's `total`, `deliveryPrice`, `status` and
  slot. A value the order data doesn't carry is `null`, never `0`.
  `productId` is what `add_item` takes, so a line can be re-added directly —
  at the price paid then, not today's. Read-only.
- `rami_levy_search_products(query, limit?)`
- `rami_levy_add_item(productId, name, price, qty?)`
- `rami_levy_view_cart()` — this tool's own list of what it has put in the
  cart since the last checkout, not a read of the website cart (see below).
  The response carries a `scope` field saying so.
- `rami_levy_remove_item(productId)`
- `rami_levy_clear_cart()`
- `rami_levy_reorder_from_history(numOrders?, minOccurrences?)` — adds onto
  whatever is already in the cart, it does not replace it. A newly reordered
  product is priced at the last price paid — the price from the most recent
  order line that carried it — which may differ from today's price. `view_cart`'s
  `total` is therefore only an estimate until the cart is synced; `serverTotal`
  (returned by every cart-mutating tool) is the authoritative number. It shares
  its selection with `suggest_reorder` — one implementation, so the preview and
  the write can't disagree — and writes to the real account immediately, so
  prefer previewing first unless a blind reorder was asked for.
- `rami_levy_check_status()` — probes both the catalog search and page 1 of
  the order history (which needs the logged-in session); returns the first
  failure, else `{ ok: true, cartSize }`.

Every cart-mutating tool (`add_item`, `remove_item`, `clear_cart`,
`reorder_from_history`) syncs the whole cart to the real account and returns
`cartTotal` (local estimate), `itemCount`, and `serverTotal` (Rami Levy's own
total from the sync response, `null` if it gave none). If the sync fails at
the transport level (`auth_expired`, `blocked_by_cloudflare`,
`network_error`), the local cart is rolled back: `ok: false` means nothing
changed, so retrying is safe. If the response carries
`resetAfterOrder: {orderId, createdAt}`, the cart was cleared after a checkout
before the change was applied (see below).

### Where the synced cart shows up on the website

Rami Levy stores the last-synced cart server-side, per account. The website
keeps its own copy of the cart in the browser, and **merges the server cart
into it only when the checkout page (`/he/dashboard/checkout`) loads**. The
home page's cart icon shows the browser's copy alone, so it won't reflect
anything this server added until the checkout page has been opened once.
Point people at the `checkoutUrl` that `rami_levy_view_cart` returns.
(Measured live on 2026-09-18.)

Because the merge runs on page load, a checkout page that was **already open
when the cart changed keeps showing the pre-change contents** — which reads
like the add silently failed. `view_cart` returns a `checkoutHint` field
saying to reload it, and the cart-mutating tools' descriptions say the same,
so the agent volunteers it instead of sending the user to a stale page.
(Hit for real on 2026-09-20: an item added seconds after the checkout page
loaded was absent in the browser and present in the account cart.)

Because that step is a merge, removals don't propagate. If the browser
already holds a product, `remove_item` or `clear_cart` deletes it from the
server cart, but the browser's copy brings it back the next time checkout
loads. Adds and quantities from this server always show up.

### The local cart vs. the real one

The server keeps its own cart in SQLite and pushes the whole thing to the
account on every change. Rami Levy's API has **no endpoint to read the cart
back**, so the local cart can drift from the real one whenever the cart
changes outside this tool:

- **Checkout.** After an order is placed, the site's cart is empty, but the
  local cart still holds everything that was bought, and the next sync would
  put the whole order back. So before every cart change (`add_item`,
  `remove_item`, `clear_cart`, `reorder_from_history`), if the local cart isn't
  empty, the server fetches page 1 of the order history. If any order was
  created after the last successful sync, those items were bought: the local
  cart is cleared first, then the change is applied, and the response includes
  `resetAfterOrder: {orderId, createdAt}` so the agent can tell the user the
  cart started fresh. If the order history can't be fetched, the change is
  not made: the tool returns the usual `ok: false` reason. It never skips the
  check. The last-sync time lives in the same SQLite file (a `meta` table,
  added automatically to an existing DB); a cart from before this upgrade
  has no sync time, so the check starts working after its first sync.
- **Edits made on the website.** Anything added, removed, or emptied in the
  browser is invisible to this server. `view_cart` shows only what this tool
  has put in the cart since the last checkout, and says so in its `scope`
  field.

Timezones: the last-sync time is stored as a UTC instant, and an order's
`created_at` is resolved to one before they're compared. Live (measured
2026-09-20) that field is zoned ISO-8601 — `"2026-09-08T05:59:37.000000Z"` —
and is taken at its word. A naive Israel-time string (`"2026-09-08 10:15:00"`,
no offset) is also accepted, and converted with the real `Asia/Jerusalem`
rules from `Intl` (UTC+2 in winter, UTC+3 in summer), never a fixed offset.

## Errors

Every tool responds with `{ ok: false, reason, ... }` instead of throwing.
Reasons and what to do about each:

| Reason | When | What to do |
|---|---|---|
| `auth_expired` | A JSON response came back 401/403 | Re-capture the token bundle above |
| `blocked_by_cloudflare` | The response wasn't JSON, at any HTTP status | Recapture the bundle first (`cf_clearance` expires). Only if a fresh capture still fails, suspect your egress IP (proxy/VPN/Israeli IP) |
| `network_error` | Fetch failed, the body was invalid JSON, the response shape was unexpected (including an order `created_at` that can't be read during the checkout check), or search ignored the query | Check connectivity; if it persists, the API may have changed (see below) |
| `items_rejected` | The cart sync succeeded but the server dropped some products (`rejected: [{productId, name}]`) | They were removed from the local cart too; search for alternatives |
| `not_in_cart` | `remove_item` was called for a product not currently in the cart (nothing was synced) | Check `view_cart` for the current contents |
| `invalid_args` | `reorder_from_history`'s `numOrders`/`minOccurrences` were out of bounds | Pass `numOrders` 1–50 and `minOccurrences` between 1 and `numOrders` |
| `internal_error` | An unexpected exception was caught at the tool boundary | Inspect the `details` field; likely a bug worth reporting |

## Verified against the live API

Search wire format, order response nesting, and the cart sync response were
all **verified against the real API on 2026-09-18** with fresh logged-in
captures from an Israeli IP, and match what the client parses. One thing to
know about the cart response: Rami Levy adds its own delivery-fee line to
`items` server-side (e.g. `{id, name: "מחיר משלוח", price, quantity}`); this
tool ignores it (it's never matched to a local cart product), and the
top-level `price` — surfaced as `serverTotal` — excludes it, so `serverTotal`
is the product total only, not what checkout will actually charge.

## Manual smoke test (not part of automated tests — needs a real, live bundle)

`RAMI_LEVY_COOKIE` is optional (see above) — leave it unset and the `Cookie`
header below is just empty, which the real API accepts fine.

```bash
curl -s -X POST "https://www.rami-levy.co.il/api/catalog" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $RAMI_LEVY_BEARER_TOKEN" \
  -H "ecomtoken: $RAMI_LEVY_ECOM_TOKEN" \
  -H "Cookie: $RAMI_LEVY_COOKIE" \
  -H "User-Agent: $RAMI_LEVY_USER_AGENT" \
  -d '{"q":"milk","store":"412"}' | head -c 600
```

It works only if the response echoes `"q":"milk"` (not `"q":null`) **and**
contains product data. A `200` with `"q":null` means the query was ignored
(the wire format is wrong). An HTML body, at any status, is a Cloudflare
challenge: recapture the bundle and retry before blaming your egress.

## The shopping skill

`skills/rami-levy-shop/SKILL.md` carries the *workflow* the tools deliberately
don't: preview with `suggest_reorder` before writing, present the candidates
and let the user cut them, then add what survived — plus the cart/website
distinction, the Hebrew-search rule, and what to do about each error `reason`.
The tools stay mechanism; the skill is policy, so changing how a shop is run
doesn't mean shipping code.

For Claude Code, point a user skill at it:

```bash
ln -s "$PWD/skills/rami-levy-shop" ~/.claude/skills/rami-levy-shop
```

A symlink rather than a copy, so `git pull` updates the skill too.

## Installing into NanoClaw

This ships as an [Agent Plugins 1.0.0](https://agent-plugins.org) plugin
(`plugin.json` + `mcp.json`) — copy this directory into a group's
`plugins/rami-levy/`, fill in the real values for the placeholder env vars
in that group's stored MCP server config, and restart the group.

TDQS

A4.6/5.0

Scored across 7 tools

Disambiguation5/5

Each tool targets a distinct operation: searching, adding, removing, clearing, viewing the local cart, checking connectivity, and reordering from history. The only possible overlap is check_status returning cart size, but it is clearly framed as a health check, not a cart read.

Naming Consistency5/5

All tools follow a consistent rami_levy_<verb>_<noun> pattern, e.g. search_products, add_item, remove_item, clear_cart. The naming convention is uniform and predictable across the entire set.

Tool Count5/5

Seven tools is well-scoped for a grocery cart integration: search, add, remove, clear, view, status, and reorder each earn their place. There is no redundancy or bloat.

Completeness4/5

The cart lifecycle is well covered with search, add, remove, clear, local view, and reorder-from-history. Minor gaps like no direct order-history listing and no quantity-update operation are workarounds but do not break core workflows.

Maintenance

ActivityMaintained
ResponsivenessNo issues