I built a public MCP server over the benefits catalogues of five Israeli clubs — Behatsdaa and Hever, two Israeli membership benefit clubs, plus the card issuers Isracard, max and Cal — merged into one searchable catalogue of 22,877 offers. I built it because some of those catalogues sit behind a tedious login, and one of them behind an SMS one-time code as well. It turned out to be the cleanest case I have run into for MCP still being worth the effort.
The problem isn’t the benefits, it’s the friction
I genuinely don’t understand why so many membership clubs put their benefits catalogue behind authentication. It stops people from using benefits they already pay for — not because the benefit isn’t worth it, but because they never find out it exists, and the path to finding out is too long.
Plenty of people have tried to fix this. My read is that those attempts died from a combination of not enough public interest and card issuers with no reason to cooperate. There’s the FID app, but I don’t find its search intuitive enough either — and above all, it makes you install an app on your phone. Why isn’t this on the web?
When an MCP server is still the right call
MCP arrived like a storm and then went out of fashion once the built-in web-search tools got good. If the model can just browse to a page and read it, why install a server?
Three conditions. My rule of thumb is that you want at least one of them before the work pays for itself:
- The data is behind a wall. No search tool will ever see a catalogue that requires a login.
- It’s messy, and reading it takes domain knowledge. “20% off” means something different depending on whether it’s credited to a prepaid card, discounted on your statement, or taken off at the register.
- There’s too much of it. 22,877 rows do not fit into a model’s context, and they shouldn’t have to.
This case hits all three. Condition 3 is overkill here, but it is what makes the thing work: the data is filtered deterministically before it ever reaches the model, and the full terms are pulled only for offers already established as relevant.
What’s in it
Five organisations, nine source groups. They stay separate because they genuinely behave differently: Hever’s blue card is a network of physical merchants, Behatsdaa’s digital wallet is a monthly top-up budget, and a voucher is a product you buy up front.
| Club | Groups | Offers |
|---|---|---|
| Behatsdaa | vouchers, digital wallet | 16,566 |
| Hever | black, yellow and blue cards, vouchers | 4,101 |
| Cal | experiences store | 1,208 |
| max | max benefits | 682 |
| Isracard | Isracard benefits | 320 |
All of it lives in one SQLite database with an FTS5 index over name, category and city. Public catalogue only — balances, monthly top-ups and a card’s last four digits are never stored, and there’s a unit test that pins that.
Harvesting was the dirty part
Not one source was collected with curl. All of them block requests that don’t come from a real browser, and two of them — Behatsdaa and Hever — also needed a signed-in session. Every one was different:
- Behatsdaa — a private JSON API that rejects calls without an
OrganizationIdheader (the app sends anativeone too). There is no bearer token — auth is HttpOnly cookies, and logging in means an ID number followed by an SMS code. The catalogue is a tree you have to walk, and the server throttles: after roughly 1,500 nodes, latency climbs from about 0.6 seconds to about 3 seconds per request, so a full sweep has to be incremental. - Hever — the blue card is a single JSON file, for signed-in users only. The yellow card simply exposes the whole array as
window.data. The credit network, by contrast, is a WebForms UI with no bulk dataset behind it, so that one is page by page. - Isracard — the entire public “benefits for everyone” catalogue is embedded as JSON inside the homepage HTML. One browser fetch is the whole harvest. No login needed, but Cloudflare blocks anything that isn’t a real browser.
- max — a private JSON API behind an Angular app. Its
clubandregionparameters take the literal stringundefined; leave them out and the server returns an empty array with no error at all. That cost me a full pass. - Cal — behind a bot-challenge layer. The listing tiles are background images with no text, so names and prices had to come from the individual product pages.
This is the part that never shows up in a demo, and it is most of the work.
The deterministic part
Each of the 22,877 rows is embedded with Titan v2 at 512 dimensions. The result is a single 22,877 × 512 numpy array sitting on disk. A semantic search is one matrix multiply and a sort — brute force is instant at this size, so there is no vector database anywhere in this.
What matters is what doesn’t happen: the catalogue never enters the context. A search returns at most 50 thin rows, and the full terms — the fine print — come back through a separate call, keyed by a specific offer id. The model sees the fine print only after the offer has already been established as relevant.
What this catalogue doesn’t know yet
Offer-level terms exist for Hever only. 1,531 pages were harvested on 28 June 2026, but only 758 of them — the credit network — actually join to catalogue rows and return terms, and in about 97% of those the crediting mechanism isn’t stated on the page at all, because it’s a rule set at the programme level rather than per offer. Behatsdaa has no offer-level terms yet, since they sit behind that same SMS code.
There are holes in the catalogue itself, too. max’s statement-credit discounts never made it in — that endpoint returns nothing without a login. Isracard’s fields that only open up for a signed-in member are empty. So is Cal’s eligibility-gated category. Freshness is uneven as well: part of the catalogue is from July 2026 and part from June. A price or condition in an answer is a starting point for checking the club’s official site, not a replacement for it.
The server itself
Six read-only tools: list_sources, search_promotions, list_categories, semantic_search, get_club_terms, get_offer_terms. The descriptions and schemas come from the same definition the chat agent behind the site is built from — one source of truth, rather than two copies that drift apart.
The transport is stateless streamable HTTP with JSON responses — no session state between calls, and a reply any plain HTTP client can read. There is no auth: it’s a public catalogue and the tools only read.
The one tool that writes, report_issue, is excluded twice — from the tool listing and again at call time. That isn’t belt-and-braces for its own sake: the SDK forwards tool names that were never listed, so the listing filter is a courtesy and the call-time guard is the actual barrier. There’s a test for it.
The whole thing runs on a single t4g.small EC2 instance in eu-central-1 at roughly $14 a month, nginx in front of uvicorn, reaching Bedrock through an IAM role instead of static keys. A capacity limiter of eight concurrent jobs keeps MCP traffic from starving the chat.
Installing it
From Claude Code, one line:
claude mcp add --scope user --transport http yesh-et-ze https://yesh-et-ze.hopala.io/mcp
From the browser: Settings → Connectors → Add → Add custom connector, then paste the same URL.
There’s a chat UI too, and it’s closed
Over the same catalogue I also built a Hebrew chat interface, with a source picker and a meter showing the estimated cost of the current conversation in shekels. It’s behind a password at the moment; the MCP endpoint is the public surface.
What I take from this
MCP hasn’t died and it hasn’t won. It’s a tool with clear conditions for use: data behind a wall, that takes knowledge to read correctly, and that there’s too much of to simply paste in. Most data in the world fails all three tests. A fair amount of the data sitting inside a company doesn’t.
If you have an internal system nobody opens because the journey to it is too long, this is how I work — starting with a map of what is genuinely locked and what only looks that way.