Lufa Farms MCP Server
# Lufa Farms MCP Server
<!-- mcp-name: io.github.FadiSheh/lufa-farms-mcp -->
> Let an AI agent do your weekly grocery shopping on [Lufa Farms](https://montreal.lufa.com/).
```
"Add two portions of heirloom tomatoes, a sourdough loaf,
and whatever goat cheese they have this week to my basket."
```
> **Disclaimer:** This is an independent personal project, not affiliated
> with, endorsed by, or officially connected to Lufa Farms in any way. It
> wraps Lufa Farms' internal, undocumented web endpoints (see [Discovered
> API endpoints](#discovered-api-endpoints)) rather than any official or
> public API.
---
## What is this?
An [MCP (Model Context Protocol)](https://modelcontextprotocol.io) server that wraps the Lufa Farms web API, exposing it as tools an AI agent (Claude, etc.) can call. You can browse the catalog, search for products, fill your basket, and inspect your upcoming order — all through natural language.
The server was reverse-engineered from the Lufa Farms web app's network traffic. It uses their internal `/superMarket/*` JSON endpoints, authenticated via a PHP session cookie.
---
## Requirements
- Python 3.12+
- A Lufa Farms account (https://montreal.lufa.com)
```bash
pip install -e .
```
This installs the `lufa-farms-mcp` (server) and `lufa-farms` (login CLI) commands.
---
## Project structure
```
MCP-LufaFarms/
├── src/lufa_farms/
│ ├── server.py # MCP server — tool declarations and dispatch
│ ├── client.py # Async HTTP client for the Lufa Farms API
│ ├── cli.py # `lufa-farms login` / `lufa-farms logout`
│ └── __main__.py # `python -m lufa_farms` entry point
├── pyproject.toml # Project metadata
└── README.md
```
---
## Setup
This is a standard stdio MCP server, so it works with any MCP-compatible
agent or client (Claude Code, Claude Desktop, Cursor, Windsurf, etc.) — not
just one. After `pip install -e .`, add it to your client's MCP server
config using the installed `lufa-farms-mcp` command:
```json
{
"mcpServers": {
"lufa-farms": {
"type": "stdio",
"command": "lufa-farms-mcp"
}
}
}
```
Where that config lives depends on the client, e.g.:
| Client | Config file |
|--------|-------------|
| Claude Code | `~/.claude.json` |
| Claude Desktop | `claude_desktop_config.json` |
| Cursor | `.cursor/mcp.json` |
| Windsurf | `~/.codeium/windsurf/mcp_config.json` |
| GitHub Copilot (VS Code) | `.vscode/mcp.json` |
Then restart the client. The tools will be available in every conversation.
---
## Available tools
### Authentication
Logging in is **not** an MCP tool — credentials must never be typed into the
agent conversation. Run this yourself in a terminal instead:
```
lufa-farms login
```
It prompts for your email and password (password hidden, via `getpass`) and
saves the resulting session to `~/.lufa_session.json`. The agent only ever
checks or clears that session:
| Tool | Description |
|------|-------------|
| `lufa_logout` | Log out and erase the saved session |
| `lufa_auth_status` | Check whether you are currently logged in |
Lufa sessions last ~30 days. If yours expires, any tool call will return a
clear message telling you to run `lufa-farms login` again — no need to watch
for raw HTTP errors.
### Products
| Tool | Description |
|------|-------------|
| `lufa_get_products` | Browse the full weekly catalog |
| `lufa_search_products` | Search by keyword (e.g. `"tomatoes"`, `"bread"`) |
| `lufa_get_product_details` | Get description, price, producer, and availability for one product |
| `lufa_get_nutritional_facts` | Get the nutritional panel (calories, macros, ingredients) |
### Basket
| Tool | Description |
|------|-------------|
| `lufa_view_basket` | See current basket contents and totals |
| `lufa_add_to_basket` | Add a product (with optional quantity) |
| `lufa_remove_from_basket` | Remove a product |
| `lufa_save_basket` | Not required — kept for backwards compatibility, see note below |
| `lufa_add_favorite` | Save a product to your favorites for next week |
`lufa_add_to_basket` / `lufa_remove_from_basket` persist to Lufa's server
immediately — there is no "confirm order" step anywhere in Lufa's actual
site (no such button or endpoint exists in the marketplace frontend), so
`lufa_save_basket` has nothing to do and is kept only for backwards
compatibility.
### Orders
| Tool | Description |
|------|-------------|
| `lufa_get_order_details` | View the upcoming order: date, pickup point, totals |
| `lufa_get_checkout_info` | View delivery date, address, time window, and a countdown to delivery |
---
## Example prompts
```
Search for goat cheese and add the cheapest one to my basket.
```
```
Show me everything in my basket and tell me the total.
```
```
Find all the bread products available this week and describe each one.
```
```
Remove the kale from my basket and replace it with spinach.
```
```
How much time do I have left before this week's order is delivered?
```
---
## How authentication works
1. A GET to `/en/login` seeds the `PHPSESSID` cookie.
2. A POST to `/en/login` submits the Yii `LoginForm` (fields: `LoginForm[user_email]`, `LoginForm[password]`).
3. The session is validated against `/superMarket/GetUserOrderDetails`.
4. All cookies are persisted to `~/.lufa_session.json` — subsequent runs reuse them without re-logging in.
---
## Delivery countdown
Lufa's API has no explicit "order cutoff" field. `lufa_get_checkout_info`
fills that gap by computing a countdown from the delivery date and delivery
window it does return: `delivery_window_starts_at` (an ISO timestamp) and
`time_until_delivery` (e.g. `"3d 22h"`). This is time until your delivery
window opens, not a confirmed order-lock deadline — Lufa may stop accepting
basket changes some time before that, but exactly when hasn't been
reverse-engineered. Treat it as a useful approximation, not a hard cutoff.
---
## Discovered API endpoints
| Method | Path | Purpose |
|--------|------|---------|
| `GET` | `/en/login` | Seed session cookie |
| `POST` | `/en/login` | Authenticate |
| `POST` | `/superMarket/getAllProducts` | Full catalog (also used to look up a single product) |
| `POST` | `/superMarket/getProductDescription` | Long description |
| `POST` | `/en/products/GetNutrionalFactsForPopUp` | Nutritional facts |
| `GET` | `/search/marketplaceSearch` | Full-text search |
| `POST` | `/superMarket/GetUserOrderDetails` | Basket contents / order summary |
| `POST` | `/en/superMarket/addToBasket` | Add to basket |
| `POST` | `/en/superMarket/removeFromBasket` | Remove from basket |
| `POST` | `/en/superMarket/save` | Legacy per-product add/set (not a basket-wide commit) |
| `POST` | `/en/superMarket/addFavorite` | Add/remove favorite (`addFavorite: 1`/`0`) |
| `GET` | `/superMarket/getManageOrderComponentData` | Delivery date, address, time window |
---
## Contributing
See [CONTRIBUTING.md](CONTRIBUTING.md).
## Security
See [SECURITY.md](SECURITY.md) for the threat model and how to report an issue.
## License
[GPL-3.0-or-later](LICENSE).
TDQS
Scored across 13 tools
Each tool targets a distinct resource/action: authentication, product discovery, basket management, favorites, and order info. Even the potentially confusing get_products vs search_products are clearly separated (full catalog vs keyword lookup). The legacy save_basket is clearly marked as a no-op but remains distinct from other tools.
Most tools follow the lufa_verb_noun pattern (get_products, add_to_basket, get_order_details). Two exceptions: lufa_auth_status (noun phrase rather than verb) and lufa_logout (verb only). Overall the prefix and verb style are consistent enough that the naming feels tidy.
13 tools is a reasonable, well-scoped set for a grocery delivery MCP server. The count covers all major workflows without bloat: auth, product browsing, basket editing, favorites, and order status. No redundant or missing categories that would make the count feel excessive or thin.
The tool surface covers the full customer journey: browse/search products, get details and nutrition, manage basket, save favorites, and view order/checkout info. Auth is represented via status and logout (login is presumably handled out-of-band). The legacy save_basket is explicitly documented as unnecessary, so no functional gap exists.