AntiCoco 🥥🚫

Serveur MCP qui donne accès à ton compte HelloFresh (France, hellofresh.fr) pour : lire le menu de la semaine, exclure des ingrédients (la noix de coco en priorité), proposer une shortlist de recettes, puis enregistrer ta sélection après confirmation.

Client MCP visé : Hermes (Nous Research), qui tourne sur le même homelab.

⚠️ HelloFresh n'a pas d'API publique. AntiCoco s'appuie sur l'API interne gw/ du site (non documentée, susceptible de changer) — usage strictement personnel.

État (validé de bout en bout, 2026-06)

Boucle complète testée sur le vrai compte : lecture du menu (menus-service/menus → détails batch recipes/recipes?ids=…), filtrage coco (4/85 détectées, faux positifs des tags internes neutralisés), proposition classée, et écriture réelle réussie (PUT /v1/carts/{week}, HTTP 200) — sélection par index de course, ids de compte dérivés dynamiquement.

Auth autonome (pur HTTP) : le token (30 min) est rafraîchi par un simple POST /gw/refresh (le endpoint que la SPA appelle), sans navigateur. Le refresh_token roule par fenêtres de 60 j, remises à zéro à chaque refresh → un homelab allumé en continu reste authentifié sans intervention ni re-sync. Le navigateur headless ne sert plus que de filet de secours.

⚠️ La chaîne de refresh peut casser (homelab éteint trop longtemps, ou refresh_token consommé hors _refresh_session) → POST /gw/refresh renvoie invalid_grant et auth_status() passe à logged_in:false. Le seul remède est de refaire le login CDP (cf. ci-dessous). Restaurer un ancien .session/ ne marche pas : les refresh_token sont rotatifs, les anciens sont morts.

⚠️ La connexion directe automatisée (Playwright/Chromium qui remplit le formulaire) est bloquée par l'anti-bot HelloFresh. La session se crée donc via attache CDP à ton vrai Chrome (tools/attach_capture.py), où le login marche normalement.

Architecture

Hermes ──HTTP──▶ server.py (FastMCP, :9200/mcp)
  navigateur ──▶ server.py (UI admin, :9200/)
                   ├─ hellofresh/auth.py    session storage_state + refresh HTTP /gw/refresh
                   ├─ hellofresh/api.py     httpx : menu, détails, deliveries, PUT cart
                   ├─ hellofresh/filter.py  exclusion (coco !) + scoring préférences
                   ├─ hellofresh/webui.py   page d'admin + API JSON (édition à chaud)
                   └─ config/               excludes.json · prefs.json · endpoints.json

Mise en route

1. Installer

pip install -r requirements.txt
playwright install chromium
cp .env.example .env

2. Créer (ou ré-créer) la session — login via TON Chrome, anti-bot contourné

C'est la seule méthode de login qui marche : un navigateur piloté par Playwright (même avec channel=chrome) est bloqué par l'anti-bot HelloFresh. On lance donc le vrai Chrome en mode debug et on s'y attache en CDP (lecture seule). Même procédure pour la 1ʳᵉ session et pour une ré-authentification après invalid_grant.

# 1) Lance ton vrai Chrome en mode debug avec un PROFIL DÉDIÉ (Chrome ≥149 refuse le debug
#    sur le profil par défaut). Ta fenêtre Chrome habituelle peut rester ouverte.
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \
    --remote-debugging-port=9222 --user-data-dir="$HOME/.hf-chrome-debug" \
    https://www.hellofresh.fr/my-account/deliveries/menu

# 2) Connecte-toi dans cette fenêtre (page de login = https://www.hellofresh.fr/login).

# 3) Pendant que Chrome reste ouvert et connecté, attache la capture :
python tools/attach_capture.py        # capture trafic + exporte .session/storage_state.json

attach_capture doit tourner pendant que tu (re)charges la page menu, pour capter le trafic. Il écrase .session/storage_state.json avec la session fraîche (cookies ~60 j) — ne pas restaurer un vieux backup à la place (refresh_token rotatif = anciens morts). Vérifie ensuite : python -c "from hellofresh import auth, json; print(auth.auth_status())"logged_in: true. config/endpoints.json est déjà rempli ; rejoue attach_capture si l'API change.

3. Tester en local (headless, comme le homelab)

python server.py        # auth via storage_state, refresh token automatique

Déploiement homelab (Docker)

.session/ et .env ne sont jamais versionnés. Workflow :

# 1. Sur le Mac : générer la session (cf. « Mise en route » §2 → .session/storage_state.json)

# 2. Pousser le code
git add -A && git commit -m "..." && git push

# 3. Synchroniser la session : storage_state.json SEUL (cookie = refresh_token vierge).
#    NE PAS copier token.json (son refresh_token a pu être consommé localement → invalide).
scp .session/storage_state.json homelab:/home/jerem/AntiCoco/.session/

# 4. Sur le homelab : supprimer l'ancien token.json (root, écrit par le conteneur) pour que
#    la chaîne reparte du cookie frais, puis déployer.
ssh homelab
cd /home/jerem/AntiCoco
docker exec anticoco rm -f /app/.session/token.json   # sinon un vieux RT mort persiste
git pull && docker compose up -d --build
# bootstrap + vérif de la chaîne (doit afficher True puis logged_in: true) :
docker exec anticoco python -c "from hellofresh import auth; print('refresh', bool(auth._refresh_session())); print(auth.auth_status())"

Vérifier : curl -s http://127.0.0.1:9200/mcp (le serveur répond au handshake MCP).

⚠️ Un seul propriétaire du refresh_token. Le refresh_token est rotatif (chaque /gw/refresh invalide le précédent). Le Mac sert UNIQUEMENT à capturer la session ; une fois storage_state.json synchronisé, ne lance plus server.py ni d'appel API depuis le Mac — sinon Mac et homelab se disputent le token et la chaîne casse (invalid_grant). Le homelab, seul consommateur, reste authentifié tant qu'il tourne. En cas de casse : refaire §2 + §3-4.

Intégration Hermes

Enregistrer AntiCoco dans la config MCP de Hermes (côté homelab), URL http://127.0.0.1:9200/mcp (transport streamable-http).

Si Hermes n'accepte que le stdio, changer la dernière ligne de server.py (mcp.run(transport="stdio")) et lancer le serveur en sous-processus — le reste est identique.

Outils MCP exposés

Outil Rôle
hf_auth_status() état de connexion
hf_login() (re)connexion + capture token
hf_list_weeks() semaines modifiables
hf_next_delivery() prochaine box réellement sélectionnée (≈4 recettes) + date/cutoff, images servables — prêt Telegram
hf_favorites() recettes favorites du compte (images servables)
hf_get_menu(week) toutes les recettes, avec contains_excluded et is_favorite
hf_propose(week, count=0) shortlist sans coco ni premium, classée par préférences
hf_confirm_selection(week, recipe_ids, allow_premium=False) écrit la sélection (refuse coco et recettes payantes hors abonnement)
hf_get_excludes() / hf_add_exclude(term) / hf_remove_exclude(term) gérer la liste d'exclusion

Les recettes premium (supplément, chargeSetting côté HelloFresh) portent is_premium / surcharge_eur dans chaque réponse. hf_confirm_selection les refuse par défaut ; allow_premium=True accepte sciemment le surcoût.

Interface web d'admin

Sur le même port que MCP : http://127.0.0.1:9200/ (MCP reste sur /mcp). Édition à chaud (prise en compte au prochain appel, sans redémarrage) de :

  • la liste d'exclusion (coco & co) ;
  • les préférences de scoring liked / disliked.

Le port 9200 est exposé sur tout le LAN (cf. docker-compose.yml), sans authentification — réseau privé de confiance. Accès direct : http://<ip-homelab>:9200/.

Configuration

  • config/excludes.json — ingrédients bannis (matching insensible casse/accents). Coco déjà listée.
  • config/prefs.json — mots-clés liked/disliked pour classer les propositions.
  • config/endpoints.json — URLs gateway réelles + transfo CDN images (image_cdn_* : les URLs recettes CloudFront …/0,0/image/X répondent en 502, réécrites vers img.hellofresh.com/…/hellofresh_s3/image/X). Sélection courante via my-deliveries/menu, favoris via cfs/v2/favorites/recipe. Re-découverte : tools/probe_selection.py / tools/probe_menu_capture.py (attache CDP).
Description
No description provided
Readme 183 KiB
Languages
Python 99.5%
Dockerfile 0.5%