Command-line interface and MCP server for nemlig.com Danish online grocery store. Single-file Python implementation using requests for HTTP and argparse for CLI parsing, plus a FastMCP server (server.py) that exposes the same client to AI assistants.
Fork of eisbaw/nemlig_cli, adding the MCP server and several API endpoints (see Additions in this fork).
This fork deliberately drops upstream's non-nemlig features (AI meal planner, Google Sheets import, fridge/barcode scanner, local grocery list with budget, interactive REPL) to keep the code focused on the nemlig.com integration.
- Product search and details
- Shopping basket management (view, add, remove items)
- Saved shopping lists (favoritter) — list them, add a whole list to the basket
- Delivery time slots — list available slots, reserve one
- Order history viewing
- MCP server exposing all of the above as tools
| Addition | nemlig_cli.py functions |
CLI | MCP |
|---|---|---|---|
MCP server (server.py) |
— | — | ✅ |
| Remove from basket | remove_from_basket |
— | delete_from_cart |
| Shopping lists | get_shopping_lists, add_shopping_list_to_basket |
— | list_shopping_lists, add_shopping_list |
| Delivery slots | get_delivery_days, update_delivery_time |
— | list_delivery_slots, set_delivery_time |
The new endpoints are library + MCP only — they have no argparse subcommand yet. Call them from Python, or through the MCP server.
- Python >= 3.11
- uv package manager
- Credentials for nemlig.com account
cd /path/to/nemlig_cli
uv tool install --editable . # puts `nemlig` and `nemlig-mcp` in ~/.local/bin--editable means edits to this checkout take effect without reinstalling; re-run with --reinstall after changing dependencies in pyproject.toml.
Both the CLI and the MCP server look for credentials in this order (first match wins):
-u/-poptions (CLI only; avoid-p, the password is visible inps)NEMLIG_USER/NEMLIG_PASSenvironment variables- macOS Keychain item with service
nemlig— the recommended setup:security add-generic-password -s nemlig -a your@email.com -w # prompts for the password ~/.config/nemlig/login.json(CLI only; should bechmod 600)
All HTTP requests use a 5 s connect / 20 s read timeout.
All commands are available via the justfile:
just search "cocio" # Search products
just details 701025 # Product details
just basket # View basket
just add 701025 2 # Add product (quantity optional)
just history # Order history
just history 12345678 # Order detailsFrom any terminal, after the global install:
nemlig search "milk"
nemlig basketserver.py wraps the client's core functions as an MCP server (stdio transport, FastMCP), so an AI assistant can search, manage the basket and book delivery slots directly.
nemlig-mcp # after the global install; or: uv run python server.pyfastmcp is a core dependency, so the install above is all the server needs.
| Tool | Read/write | Description |
|---|---|---|
search_products(query, limit=10) |
read | One line per product: [id] name (brand) — price (unit price) — description [stock] |
get_product_details(product_id) |
read | Description, price, unit price, contents, nutrition, availability |
view_basket() |
read | One line per basket item plus the total |
order_history(limit=10, order_id=None) |
read | Recent orders, or one order's line items |
list_shopping_lists() |
read | Saved lists: ID, name, product count, total |
list_delivery_slots(days=8) |
read | Bookable slots per day: ID, window, price; marks the selected slot and flex (uden opsyn) slots |
add_to_basket(product_id, quantity=1) |
write | Adds to the live basket |
delete_from_cart(product_id) |
write | Removes a product line from the live basket |
add_shopping_list(list_name_or_id) |
write | Adds every product from a saved list (name is case-insensitive) |
set_delivery_time(timeslot_id) |
money | Reserves a delivery slot for ~20 min; final at checkout — confirm date/window/price with the user first |
Write tools operate on the account's real basket. Their docstrings say so, so the model asks before mutating.
- Auth caching — logs in once and reuses the bearer token for ~4.5 min (tokens expire around 5 min); a 401/403 triggers one re-login and retry.
- Rate limiting —
requests.Session.requestis patched to a max of ~2 requests/sec, throttling every call including the internal login steps. nemlig.com's API is undocumented; be polite. - Silent spinner —
nemlig_cli.Spinneris replaced with a no-op so nothing animated can reach stdout, which is the JSON-RPC channel under stdio transport. - Errors — API failures become clear
ToolErrormessages ("the endpoint may have changed from what nemlig_api.md documents") instead of raw tracebacks. - Output — compact structured text with IDs in brackets, never raw JSON, to keep responses small in the model's context.
After the global install (see Install), nemlig-mcp is on PATH and finds credentials itself (environment, then Keychain), so nothing secret goes in any config file. Register it per project — run this inside the project folder that should have the tools:
claude mcp add nemlig --scope project -- nemlig-mcp # writes ./.mcp.jsonwhich is equivalent to this .mcp.json in that folder:
{
"mcpServers": {
"nemlig": { "type": "stdio", "command": "nemlig-mcp", "args": [] }
}
}Use --scope local instead to keep it out of the project's git (stored privately per folder in ~/.claude.json).
Note that this repo's own .mcp.json configures the Chrome DevTools MCP used for API discovery (see the development workflow below) — not this server.
Single file design: All client logic in nemlig_cli.py - a straightforward requests-based client. server.py is a thin MCP layer on top: it imports nemlig_cli and calls the same functions, adding auth caching, throttling and output formatting. New endpoints go in nemlig_cli.py; server.py only exposes them.
Authentication: 3-step flow (XSRF token -> Bearer token -> Login). Returns AuthTokens dataclass passed to all API functions.
Dual API endpoints: Main site API (nemlig.com/webapi/*) for auth and basket operations; separate search gateway (webapi.prod.knl.nemlig.it) for product search.
Endpoints used by the fork's additions:
| Operation | Endpoint |
|---|---|
| Remove from basket | POST /webapi/basket/AddToBasket with quantity: 0, AffectPartialQuantity: true (no dedicated delete endpoint exists) |
| Shopping lists | GET /webapi/ShoppingList/GetShoppingLists |
| Add list to basket | POST /webapi/basket/addShoppingListToBasket |
| Delivery days/slots | GET /webapi/v2/Delivery/GetDeliveryDays |
| Reserve slot | POST /webapi/Delivery/TryUpdateDeliveryTime?timeslotId=… (empty body) |
Slot Availability is 0 when bookable; Type is 0 for a personal delivery and 1 for flex/uden opsyn (unattended).
See nemlig_api.md for complete API documentation including request/response schemas.
This project was built by having Claude Code control a real browser to observe and document the nemlig.com API. The technique generalizes to any web application where you need to reverse-engineer an undocumented API.
The workflow enables an AI assistant to control a real browser, observe network traffic, and document API behavior - then implement a client based on the documented findings.
The Chrome DevTools MCP integration is fully self-contained:
.mcp.json- MCP server configuration pointing to the wrapper scriptchrome-devtools-mcp-wrapper.sh- Nix-shell wrapper ensuring reproducible environment with:- Pinned nixpkgs (nixos-25.05) for reproducibility
- Node.js 22 and Chromium from nix
- Project-local Chrome profile (
.chrome-profile/) to avoid tainting global settings - Pinned
chrome-devtools-mcpversion (0.10.1)
No global installation required - the wrapper script handles everything.
On machines without nix, chrome-devtools-mcp-wrapper-macos.sh is the drop-in counterpart: same pinned chrome-devtools-mcp version and same project-local profile, but run via npx against the system Google Chrome.
Phase 1: Network Traffic Capture
Human operator directs Claude to:
- Open target page with network recording enabled
- Perform the operation being documented (login, search, add to cart, etc.)
- List network requests to see all HTTP traffic
- Get request details for interesting endpoints (headers, body, response)
Example session:
Human: Open nemlig.com and enable network recording. Then login with test credentials and show me the network traffic.
Claude: [Uses Chrome DevTools MCP to navigate, perform login, capture traffic]
[Lists network requests, identifies auth flow]
[Documents the 3-step auth: AntiForgery -> Token -> login]
Phase 2: Documentation
Claude analyzes captured traffic and documents:
- Request URLs, methods, headers
- Request/response body structure
- Authentication requirements
- Parameter meanings
This builds up nemlig_api.md incrementally.
Phase 3: Implementation
Based on the documented API:
- Claude implements Python functions matching documented endpoints
- Human tests implementation against real site
- Debug issues using Chrome DevTools MCP as grounding (compare browser vs client behavior)
Important: MCP tool calls return large responses (>25KB for page snapshots/network dumps). To manage context window size:
- Run all MCP interactions from a sub-agent (Task tool with explore or general-purpose agent)
- Sub-agent summarizes findings and returns only relevant info
- Main conversation stays focused on implementation
Example pattern:
Human: Document the basket API
Claude: [Spawns sub-agent to handle MCP interactions]
Sub-agent: [Opens page, enables recording, adds item to basket]
[Captures AddToBasket request/response]
[Returns summary: endpoint, headers, body format, response structure]
Claude: [Updates nemlig_api.md with documented endpoint]
When the Python client behaves differently than expected:
- Perform same operation in browser via MCP
- Compare exact request headers/body
- Identify missing headers, wrong parameter format, etc.
- Fix client implementation
This provides a reliable reference for expected API behavior.
nemlig-cli/
├── .mcp.json # Chrome DevTools MCP config (API discovery)
├── chrome-devtools-mcp-wrapper.sh # Nix-shell wrapper for the DevTools MCP
├── chrome-devtools-mcp-wrapper-macos.sh # macOS/npx counterpart (system Chrome)
├── .chrome-profile/ # Local browser profile (gitignored)
├── arch_api.drawio.svg # API architecture diagram
├── mcp-workflow.drawio.svg # MCP workflow diagram
├── nemlig_api.md # API documentation (built via workflow)
├── nemlig_cli.py # Python client implementation
├── server.py # FastMCP server wrapping the client
├── .env.example # Credential template for the CLI/justfile
├── justfile # Command shortcuts
├── pyproject.toml # Python project config
└── CLAUDE.md # AI assistant instructions
MIT