lidl-mcp
Provides tools to interact with Lidl Plus, enabling reading receipts and managing coupons for Lidl accounts in the Netherlands and Germany.
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@lidl-mcpShow my recent receipts"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
lidl-mcp
Self-hosted MCP-server voor Lidl Plus (NL + DE) — kassabonnen uitlezen en coupons
beheren. Analoog aan ah-mcp, gebouwd op de reverse-engineered lidl-plus
library. Zie LIDL_MCP_HANDOVER.md voor de volledige
achtergrond en ontwerpbeslissingen.
Ontwerpkeuzes (uit de bouw-sessie)
NL + DE: één server, elke tool-call neemt een
country-parameter (NL/DE).Coupon-activatie is inbegrepen (
lidl_activate_coupon/lidl_deactivate_coupon) — dit zijn schrijf-acties op het echte Lidl-account.Geen database. Alles wordt live bij de Lidl-API opgehaald, geen lokale cache.
Login gebeurt buiten de container. De
lidl-plus-library logt in via headless Selenium/Chrome met een synchrone 2FA-callback — te zwaar en te fragiel om in deze lichte MCP-image te bakken. In plaats daarvan: je haalt lokaal, eenmalig per land een refresh-token op via de CLI, en die zet je als environment variable in de Portainer-stack. Er is dus bewust geenlidl_login-tool.Refresh-tokens zijn niet officieel gedocumenteerd qua levensduur, maar in de praktijk (zelfde tokenmechanisme als de Home Assistant Lidl Plus-integratie) blijven ze weken tot maanden geldig — herhaling is incidenteel, geen doorlopende taak.
Related MCP server: Willys MCP Server
Tools
Tool | Type | Functie |
| read | Check of er een geldig token is voor NL/DE (eerste stap bij problemen) |
| read | Lijst recente kassabonnen (datum, winkel, totaal, ticket-id) |
| read | Volledige itemlijst van één bon (prijs, korting, barcode) |
| read | Lijst beschikbare/geactiveerde coupons |
| write | Activeer een coupon op het echte account |
De schrijf-tool is gemarkeerd met readOnlyHint: false in de tool-annotations, zodat
MCP-clients die dat respecteren automatisch voorzichtiger zijn / om bevestiging vragen.
lidl_deactivate_coupon bestaat wél in de code (_handle_set_coupon(activate=False) in
server.py) maar is bewust niet als tool geregistreerd — Steno wil
deactivatie niet aanbieden, geen agent kan 'm dus aanroepen. Zie de code-commentaar ter
plekke voor de reden.
Transports — twee endpoints, verschillende clients
De server biedt twee MCP-transports tegelijk aan (zie server_sse.py),
elk voor een andere client:
Endpoint | Transport | Voor | Secret-vorm |
| Legacy SSE ( | Home Assistant MCP Client-integratie | query-param: |
| Streamable HTTP (één endpoint, GET+POST+DELETE) | Claude.ai Custom Connectors | pad-segment: |
Beide gebruiken dezelfde MCP_SSE_SECRET-waarde, alleen anders doorgegeven.
Waarom twee transports? Claude.ai's Custom-Connector-setup verwacht standaard de
modernere Streamable-HTTP-transport en doet een POST naar de opgegeven URL. Onze
legacy-SSE-route ondersteunt alleen GET, dus dat werd 405, waarna Claude terugvalt op
OAuth-discovery (.well-known/oauth-*, /register) — die altijd faalt (geen OAuth-server)
en een misleidende "Couldn't register with Lidl's sign-in service"-foutmelding geeft.
Bevestigd via live container-logs (2026-08-14), zelfde patroon als eerder bij ah-mcp
opgelost. Zie de steno-infra-patterns-skill voor de generieke versie van deze les
(geldt voor élke toekomstige zelf-gehoste MCP-server, niet alleen dit project).
Onderweg ook een losstaande bug gevonden en gefixt: de legacy /sse-handler was een
gewone async def-functie die zelf al rechtstreeks over de ASGI send() schreef maar
niets terugstuurde — Starlette's Route wrapt zulke functies via request_response(),
die een Response-retourwaarde verwacht en crasht (TypeError: 'NoneType' object is not callable) zodra de verbinding normaal afsluit. Bleef verborgen in eerdere tests omdat die
de verbinding altijd hard afkapten (curl -m 2) i.p.v. netjes lieten sluiten. Gefixt door
beide handlers class-based te maken (Starlette behandelt class-instances als rauwe
ASGI-apps, geen wrapping).
Login / refresh-token ophalen (lokaal, per land)
Aanbevolen: handmatige OAuth-code-capture (scripts/manual_login.py)
De ingebouwde lidl-plus ... auth-CLI stuurt Selenium een gescripte browser-sessie
door Lidl's inlogscherm. Lidl heeft dat scherm intussen vernieuwd (de library-selectors
zijn verouderd, zie PR #25) en er zijn
meldingen dat Lidl automatisch/Selenium-gestuurde logins actief detecteert en blokkeert.
Gebruik daarom liever dit script: het genereert dezelfde OAuth-login-URL, jij logt daarmee
gewoon in je eigen echte browser in (geen automatisering die Lidl kan herkennen), en
plakt de code uit de laatste (mislukkende) redirect terug in de terminal.
pip install -e .
python scripts/manual_login.py -c NL -l nlOpen de getoonde URL in je eigen browser en log in zoals gebruikelijk (inclusief 2FA).
Aan het eind probeert de browser te navigeren naar
com.lidlplus.app://callback?code=...— dat mislukt altijd in een gewone browser (geen app geregistreerd voor dat schema). Dat is verwacht: de foutmelding of adresbalk toont dan de volledige URL met de code erin.Plak die (volledige URL of alleen de code) terug in de terminal → het script wisselt 'm in en print het
refresh_token.
Herhaal voor Duitsland met -c DE -l de.
Alternatief: de lidl-plus CLI (Selenium, kan momenteel vastlopen)
Vereist: Python 3.11+ en Chrome. Werkt alleen zolang Lidl's inlogscherm overeenkomt met wat de library verwacht — momenteel dus niet gegarandeerd (zie hierboven).
pip install ".[auth]"
lidl-plus -c NL -l nl -u <telefoonnummer of e-mail> --2fa phone auth
# volg de 2FA-prompt in de terminal → refresh_token verschijnt in de outputLet op: installeer niet los
pip install "lidl-plus[auth]"— dat trekt de nieuwsteblinkerenpyOpenSSLbinnen, enselenium-wire(onderhoudt zelf al jaren niets meer) is met geen van beide compatibel:
nieuwe
blinkermist het interne, inmiddels verwijderdeblinker._saferef-attribuut → CLI toont de misleidendeCan't connect to web browser. Please install Chrome, Chromium or Firefox, terwijl Chrome prima werkt (verbergt eenModuleNotFoundError: No module named 'blinker._saferef')nieuwe
pyOpenSSLmistX509.get_extension(), waar de meegeleverde mitmproxy-fork op leunt voor TLS-cert-interceptie tijdens de login → crasht pas ná het wachtwoord-prompt metAttributeError: 'X509' object has no attribute 'get_extension'De
[auth]-extra in pyproject.toml pint daaromblinker==1.4enpyOpenSSL==22.1.0. Gebruik dus altijdpip install ".[auth]"(uit de project-root), niet het losse package.
(-p <wachtwoord> kan als vlag, maar laat 'm weg om interactief te worden gevraagd i.p.v.
in je shell-historie te belanden. --2fa email als je liever een code per e-mail krijgt.)
Herhaal voor Duitsland met -c DE -l de als je dat ook wilt gebruiken.
Losse bonnen/coupons opvragen kan ook direct via de CLI, handig om te verifiëren dat een token werkt zonder de MCP-server erbij te halen:
lidl-plus -c NL -l nl -r <refresh_token> receipt --all
lidl-plus -c NL -l nl -r <refresh_token> couponBewaar het resulterende refresh-token niet in dit repo — zet het direct in de
Portainer-stack env vars (zie hieronder), of lokaal in een .env (zie .env.example,
staat in .gitignore).
Als een token later verlopen blijkt (lidl_check_status of een andere tool meldt een
auth-fout): herhaal deze stap en werk de env var bij.
Bekende API-quirks (lidl-plus library, gemeten 2026-08-14)
Twee losse problemen die je kunt tegenkomen zodra je met een geldig refresh-token
daadwerkelijk data probeert op te halen — geen van beide is een bug in dit project,
maar in de onderliggende lidl-plus-library / Lidl's eigen API:
WAF-tarpit op de "app-headers". De library stuurt standaard drie headers mee bij elke geauthenticeerde aanvraag om zich voor te doen als de officiële app:
App-Version: 999.99.9,Operating-System: iOs,App: com.lidl.eci.lidl.plus. Lidl's WAF lijkt precies deze (overduidelijk statische/nep) fingerprint te herkennen en te tarpitten: de TLS-handshake slaagt gewoon, maar de server stuurt daarna 0 bytes terug tot de timeout — geen foutcode, gewoon stilte. Verwijder je die drie headers en stuur je alleenAuthorization(+Accept-Language) mee, dan antwoordt dezelfde endpoint binnen ~0,3s gewoon met 200 en echte data.lidl_client.pypatcht dit automatisch weg via_strip_waf_fingerprint_headers()— dus met de tools uit dit project heb je hier verder geen last van, maar als je de kalelidl-plus-CLI of -library ergens anders rechtstreeks gebruikt, loop je hier alsnog tegenaan.Coupon-lijst-endpoint (v2) is dood.
LidlPlusApi.coupons()(GET/api/v2/{country}) geeft een kale 404 — waarschijnlijk een endpoint dat Lidl heeft opgeruimd. Het ouderecoupon_promotions_v1()(GET/app/api/v1/promotionslist) werkt wél en levert dezelfde soort data. Dit project gebruikt daarom overalcoupon_promotions_v1()/activate_coupon_promotion_v1()in plaats vancoupons()/activate_coupon(). Er is geen v1-tegenhanger voor deactiveren — die tool is daarom bewust niet geregistreerd (zie_handle_set_couponinserver.py).Coupon-activatie werkt alleen voor
"Standard"-type coupons, niet voor"AssignablePromotion"-type coupons (bijv. persoonlijk toegewezen aanbiedingen). Getest (2026-08-14): een"Standard"-coupon activeren viaactivate_coupon_promotion_v1()geeft gewoon HTTP 200 enisActivatedklopt daarna. Dezelfde aanroep op een"AssignablePromotion"-coupon geeft een echte 404 van Lidl's eigen API (dus geen WAF-blokkade meer, na de header- en Content-Length-fixes) — er is geen bekend werkend activatie-endpoint voor dit coupon-type in deze library.lidl_activate_couponcontroleert nu wel expliciet de HTTP-status en geeft een duidelijke foutmelding i.p.v. ten onrechte "geactiveerd" te melden (was zelf een bug, ondertussen gefixt).coupon_promotions_v1()dedupliceert niet per winkel-instantie. Generieke acties (bijv. "Wiedereröffnung", "Aktionsrabatt") komen meerdere keren voor in de lijst — één keer per winkel — met identieke titel/korting/geldigheidsdatum maar een anderepromotionId. De Lidl-app/website groepeert die kennelijk tot 1 zichtbare coupon; deze library niet. Getest (2026-08-14): aantal unieke combinaties van (titel, korting, startdatum) kwam voor Steno's account exact overeen met het aantal dat de website toonde (17 AllStores + 11 OnlineShop = 28 = 15 winkel + 13 online). De app zelf toont daarnaast nog extra content (bijv. een "jetzt spielen"-swipe-spel, combi-coupons met meerdere producten) die niet via deze API-endpoints beschikbaar blijkt — een bekende dekkingslacune, geen bug.Het
availability.text-veld bevat vrijwel altijd generieke boilerplate-tekst ("Mit diesem Coupon haben wir leider Schwierigkeiten...") — ook bij coupons die nooit aangeraakt zijn en gewoon activeerbaar/bruikbaar zijn. Dit veld is geen betrouwbare indicator of een coupon werkt; gebruik de HTTP-status van de activatie-aanroep zelf (zie punt 3) enisActivatedin een verse her-ophaal, niet dit tekstveld.AssignablePromotion-coupons zijn wél te activeren — maar niet via de mobiele API. De website (
www.lidl.de/prm/...) gebruikt een apart endpoint:POST https://www.lidl.de/prm/api/v1/{country}/promotions/{id}/activation?language={taal}(let op: hetid-veld, nietpromotionId). Bevestigd werkend (2026-08-14) — Steno activeerde hiermee "Grafschafter Goldkrüstchen" enisActivatedklopte daarna ook via de mobiele API. Dit endpoint gebruikt echter cookie/CSRF-gebaseerde websessie-auth (in plaats van de OAuth refresh-token-flow die de rest van dit project gebruikt) — een sessie die maar ~1 uur geldig blijft, geen refresh-token-achtige langlevende variant. Nog niet geïmplementeerd in dit project: zou een tweede, apart login-mechanisme vereisen (web-sessie + CSRF-token onderhouden) voor een relatief smalle usecase. Zie git-geschiedenis/chatlog voor de volledige curl indien dit later alsnog gebouwd wordt.
Lokaal draaien
cp .env.example .env
# vul LIDL_NL_REFRESH_TOKEN (en/of LIDL_DE_REFRESH_TOKEN) in .env in
docker compose up --build
curl http://localhost:8000/healthDeployment — Portainer-stack op de HA-host (steno-infra-patterns-skill)
Geverifieerd tegen de bestaande stacks (vault-broker-mcp, npm-mcp) op dezelfde
Portainer-omgeving (endpoint "primary", id 1):
Git-repository-methode. Maak een leeg GitHub-repo (bijv.
StenoJS/lidl-mcp, zelfde account alsvault-broker-mcp/nginx-proxy-manager-mcp), en push dit repo ernaartoe:git remote add origin https://github.com/StenoJS/lidl-mcp.git git branch -M main git push -u origin mainDeploy daarna de Portainer-stack met
StackCreateDockerStandaloneRepository(RepositoryURL= bovenstaande,ComposeFile=docker-compose.yml,endpointId= 1) — of handmatig via Portainer UI → Stacks → Add stack → Repository.Netwerk:
hassio(extern, gedeeld met de andere MCP-stacks) — al indocker-compose.ymlopgenomen.Poort:
8934(host) →8000(container) — vrij op dit moment; check bij twijfel welke poorten al in gebruik zijn (ah-mcp: 3000/9876,supermarkt-mcp: 8000,vault-broker-mcp: 8933, playwright-mcp's: 8931/8932).Secrets als stack-environment-variables (Portainer UI, niet in de compose-YAML of dit repo):
LIDL_NL_REFRESH_TOKEN,LIDL_DE_REFRESH_TOKEN,MCP_SSE_SECRET(genereer metpython -c "import secrets; print(secrets.token_hex(16))").Nginx Proxy Manager (HA-add-on, UI op poort 81 van de HA-host): nieuwe Proxy Host, bijv.
lidlmcp.steno.nl→ forward naarlidl-mcp:8000(containernaam, via het gedeeldehassio-netwerk) of naar<host-ip>:8934(published poort) — beide werken, eerste is consistenter met hoevault-broker-mcphet doet. SSL via Let's Encrypt (automatisch). Geen speciale NPM-config nodig voor het secret — dat wordt nu applicatie-side gecheckt (MCP_SSE_SECRET, zieserver_sse.py), NPM hoeft alleen het volledige pad+querystring gewoon door te geven, wat het standaard al doet. Client-URL's worden dan (zie Transports hierboven voor waarom er twee zijn):HA MCP Client:
https://lidlmcp.steno.nl/sse?key=<MCP_SSE_SECRET>Claude.ai Custom Connector:
https://lidlmcp.steno.nl/mcp/<MCP_SSE_SECRET>
HA MCP Client-integratie: nieuwe instance toevoegen naast de bestaande twee (ah-mcp, nl-supermarkt-mcp), wijzend naar de
/sse-URL uit stap 5. Interactieve config-flow (Instellingen → Apparaten & diensten → Integratie toevoegen → MCP Client).Conversation-agent-prompt: uitgebreid met een derde tool-beschrijving — gebruik
lidl-mcpspecifiek voor vragen over eigen Lidl-aankopen/bonnen/coupons, niet voor prijsvergelijking (dat blijftnl-supermarkt-mcp). Zit in deconversation-subentry van de Google Generative AI-integratie in HA (niet in dit repo — HA-config).Claude.ai Custom Connector (optioneel, los van de HA-integratie): Instellingen → Connectors → Add custom connector → de
/mcp/<secret>-URL uit stap 5.
Alle 8 stappen zijn doorlopen voor de live deployment (2026-08-14) — dit is dus ook een logboek van wat er staat, niet alleen een handleiding voor de eerste keer.
Beperkingen / risico's
lidl-plusis niet-officieel, reverse-engineered van de Lidl Plus-app-API en kan zonder waarschuwing stoppen met werken.Response-schema's van
tickets()/ticket()/coupons()zijn niet door Lidl gedocumenteerd; deze server geeft de JSON grotendeels ruw door in plaats van op veldnamen te gokken die kunnen wijzigen.Coupon-activatie is een echte schrijf-actie op het Lidl-account — er is geen "dry run".
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Servers
- AlicenseAqualityAmaintenanceMCP server for managing Yazio user & nutrition data (unofficial)1519755MIT
- Alicense-qualityDmaintenanceAn MCP server for Sweden's largest grocery chain Willys. Enables controlling your shopping cart, browsing orders, searching products, and getting AI-powered recommendations from any MCP client.4MIT
- AlicenseAqualityCmaintenanceMCP server to access Costco warehouse receipts and online orders via their internal GraphQL API.72MIT
- AlicenseAqualityBmaintenanceA local MCP server for grocery shopping, enabling product search, specials, and browsing across NZ supermarkets, with cart and order history for Countdown/Woolworths via browser-assisted login.141MIT
Related MCP Connectors
MCP server for Withings health data — sleep, activity, heart, and body metrics.
MCP server wrapping the Tesla Fleet API and TeslaMate API
Open-source MCP server for Zerodha Kite Connect. Portfolio, market data, backtesting, alerts.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/StenoJS/lidl-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server