MCP Tracking
[tests] [License: MIT] [Python 3.11+]
English summary. Local-first tracking of long-running tasks: an MCP server for Claude or Lyra, a small HTTP API on 127.0.0.1:8765 and a Textual terminal dashboard. Sessions have items, progress, logs and templates (download, machine, free, lyra_task, movie); pollers feed qBittorrent and Bazarr sessions automatically. Install: pip install . then mcp-tracking, mcp-tracking-api, mcp-tracking-ui. No cloud, no telemetry.
Serveur MCP de suivi en temps reel avec dashboard terminal.
Permet a Claude/Lyra de tracker n'importe quelle operation longue ET alimente automatiquement
les sessions depuis le media-server (qBittorrent, Bazarr, conversion DV).
Pourquoi c'est conçu pour des agents LLM
Un agent LLM (grand modèle de langage) qui lance une tâche longue (conversion, clone de VM,
téléchargement) ne peut pas rester bloqué à la regarder, et il ne voit rien de ce qui se
passe entre deux de ses tours. Ce serveur lui sert de mémoire de travail partagée :
- Un bus de progression commun. L'agent, les scripts shell et les pollers écrivent
dans les mêmes sessions (outils MCP ou API HTTP locale sur 127.0.0.1:8765) : l'agent
lance la tâche, rend la main, et relit l'état plus tard avec
tracking_get.
- Des métriques calculées, pas devinées. Vitesse lissée sur 2 minutes, ETA, temps
écoulé (
metrics.py) : l'agent reçoit des chiffres au lieu d'extrapoler lui-même.
- Une tâche muette se voit. Une session
running sans mise à jour depuis 10 minutes
est marquée stale : l'agent distingue « encore en cours » de « processus mort ».
- Arrêter vise le bon processus. Une session peut porter le
pid de son processus ;
l'heure de démarrage relevée dans /proc évite de signaler un pid réutilisé par un
autre programme. tracking_stop envoie SIGTERM et garde la session, tracking_kill
envoie SIGKILL et la supprime.
- Pas de mise à jour perdue. L'état est un fichier JSON local, protégé par un verrou
flock et réécrit de façon atomique (storage.py) : plusieurs agents et scripts peuvent
écrire en même temps.
- Des gabarits par type de tâche (
download, machine, movie, lyra_task, free)
pour que chaque agent décrive son travail de la même façon.
Local-first et self-hosted : aucun cloud, aucune télémétrie, tout reste sur la machine
(on-prem). Utilisé par Lyra, l'assistant vocal
French-first, et par n'importe quel client MCP (Claude Code, Claude Desktop...).
Demo
[Dashboard terminal alimente par les simulations du mode test]
Enregistree avec docs/demo/record.sh : server.py --test alimente quatre sessions simulees dans un repertoire d'etat temporaire (TRACKING_STATE_DIR), puis server.py --ui ouvre le dashboard dessus. Les sessions reelles ne sont pas touchees.
Sommaire
Architecture
Fichiers d'etat et configuration
Un ancien tracking_state.json a cote du code est migre automatiquement au premier
demarrage (copie, jamais supprime).
Variables d'environnement de retention :
Flux de donnees complet
Le fichier d'etat est ecrit a chaque modification via ecriture atomique (os.replace) sous verrou
fichier (tracking_state.lock). Tous les processus (MCP, API, poller, dashboard) partagent cet
unique fichier ; chaque lecture verifie le mtime pour invalider son cache.
Toute mutation (HTTP ou MCP) passe par mutations.py, qui garantit le meme comportement sur les
deux chemins : started_at / finished_at poses sur les items et la session, historique de
progression (fenetre glissante de 40 points), niveaux de log info / warn / error,
auto-completion quand tous les items sont termines.
Metriques derivees
GET /sessions et tracking_get renvoient un bloc metrics calcule a la volee par metrics.py :
Installation en une ligne
Configuration Claude Desktop / Claude Code (mcpServers) :
Installation
Le MCP est enregistre dans Claude Code (scope user) :
Pour reenregistrer :
Services systemd
Deux services tournent en permanence et se lancent au boot :
Installation initiale et redeploiement
Les instances MCP server.py deja ouvertes par des sessions Claude Code ne sont pas
redemarrees par deploy.sh : reconnecter tracking via /mcp dans ces sessions.
Commandes utiles
Lancement
Dashboard (raccourci wofi)
Cherche "MCP Tracking" dans wofi/launcher. Lance le dashboard dans Kitty.
Dashboard (terminal)
Via outil MCP (depuis Claude/Lyra)
Mode test (demo)
Simule 4 sessions en parallele : download, machine (12 noeuds), free, movie (pipeline DV complet).
Dashboard
Layout d'une session
Icones items
Couleurs de session
Raccourcis clavier
Modales stop/kill
Appuyer sur s ou k ouvre un modal avec un champ de saisie pour l'ID de session.
s marque la session en paused et ajoute un log
k supprime definitivement la session du dashboard
Echap annule
Filtrage dynamique
Le cycle de filtres est construit automatiquement depuis les sessions presentes :
all toujours present
- Chaque template present dans le JSON s'ajoute automatiquement
errors n'apparait que si au moins une session a une erreur
- Filtre actif affiche dans le sous-titre :
filtre: movie | 2/5 session(s)
- Si le template filtre disparait du JSON, retour automatique a
all
Integration media-server
qBittorrent (automatique)
Le poller interroge http://localhost:8080/api/v2/torrents/info toutes les 10 secondes.
- Un torrent actif = une session
[DOWNLOAD] avec nom, taille, vitesse, ETA
- La session est supprimee automatiquement quand le torrent termine ou disparait
- Credentials :
credentials/qbt-password.cred (chiffre systemd-creds --user, genere par media-server/scripts/secrets/rotate-secrets.sh)
Bazarr sous-titres manquants (automatique)
Le poller interroge l'API Bazarr toutes les 60 secondes.
- Une session
[SUBTITLES] unique liste tous les episodes/films sans sous-titres FR
- Le titre de la session indique le total :
Sous-titres manquants (151)
- Les 50 premiers fichiers manquants sont listes comme items
- API key Bazarr :
credentials/bazarr-api-key.cred (meme mecanisme). Sans credential, le poll concerne est simplement desactive.
Conversion Dolby Vision (automatique)
Declenche par dv-webhook.service quand Radarr/Sonarr importent un film DV Profile 4 ou 7.
Flux :
Les 6 etapes trackees avec leurs metriques :
La barre de progression globale avance en continu pendant chaque etape
(pas par sauts de 1/6 a la fin de chaque etape).
Mode manuel :
En mode manuel, la session tracking est creee automatiquement dans process_file.
API HTTP locale (port 8765)
Scripts externes peuvent creer/modifier des sessions directement :
Corps PUT complet (tous les champs optionnels) :
Outils MCP
tracking_create
tracking_update
tracking_log
Ajoute un log sans modifier la progression.
tracking_complete
Marque done a 100%.
tracking_error
Marque en erreur (prefixe "ERREUR:" auto, remonte dans colonne Erreurs).
tracking_stop
Arrete proprement une session (status -> paused). Reste visible dans le dashboard.
tracking_kill
Supprime une session en force. Disparait immediatement du dashboard.
tracking_get
Retourne l'etat complet formate d'une session.
tracking_list
tracking_delete
Supprime une session (equivalent de tracking_kill).
tracking_templates
Affiche la liste des templates et leurs champs.
open_tracking_ui
Ouvre le dashboard dans un terminal Kitty.
Templates
download
Telechargement de fichiers. Alimente automatiquement par qBittorrent via le poller.
machine
Operations sur des machines (update, clone, snapshot, deploy).
Utilise par Lyra pour les operations VM/cluster.
free
Format libre. Utilise par le poller pour les sous-titres Bazarr manquants.
lyra_task
Operations Lyra (VM clone, backup, update, snapshot).
movie
Pipeline complet d'un film : telechargement -> conversion Dolby Vision.
Alimente automatiquement par dv_convert.py quand Radarr/Sonarr importent un fichier DV P4/P7.
Securite
Modele de menace
Deux attaquants realistes sur une machine de bureau :
- Une page web ouverte dans le navigateur. « localhost n'est pas une frontiere » : une page
peut emettre des requetes vers
127.0.0.1:8765. Sans protection, elle pourrait creer des
sessions, en supprimer, et surtout demander l'envoi d'un signal a un processus.
- Un autre utilisateur local (ou un service compromis) qui tenterait de lire l'etat ou de
piloter l'API.
Mesures
api.py ecoute uniquement sur 127.0.0.1:8765 -- inaccessible depuis le reseau
- Jeton local obligatoire en ecriture : genere au premier demarrage dans
$XDG_RUNTIME_DIR/tracking/token (droits 0600, donc illisible par un autre utilisateur), exige
en Authorization: Bearer ... sur POST, PUT et DELETE. Une page web ne peut pas le lire. Recuperation par les clients : voir docs/api-token.md.
- Refus journalises : chaque 401 ou 403 laisse une ligne
refus <code> <methode> <route> (<raison>) client=<User-Agent> dans le journal du service (journalctl -u tracking-api), au
plus une toutes les 10 minutes par raison et par route (table bornee a 256 entrees) ; les
valeurs fournies par le client sont nettoyees (caracteres de controle retires, 80 caracteres
au plus, ecrites entre guillemets) et le jeton envoye n'y figure jamais.
Un client qui oublie le jeton et avale l'erreur se voit donc la.
- Requetes de navigateur refusees : tout en-tete
Origin donne un 403, meme avec le jeton.
- En-tete
Host verifie (boucle locale uniquement, port d'ecoute verifie s'il est precise) et Content-Type: application/json exige
en ecriture.
- Signaux limites aux processus enregistres par le serveur : a la creation d'une session, le
serveur verifie que le
pid annonce existe et appartient au meme utilisateur, puis releve son
heure de demarrage. /stop et /kill refusent d'agir si cette empreinte a change (numero de
processus recycle par un autre programme) ou si le pid n'a jamais ete enregistre. Le pid ne
peut plus etre modifie par un PUT. Chaque signal envoye est journalise.
- Versions verifiables :
uv.lock controle en CI, et chaque release GitHub joint sbom.json
(inventaire CycloneDX des dependances aux versions du lock) ; wheel et sdist portent une
attestation de provenance et une attestation SBOM signees par Sigstore
(gh attestation verify <fichier> --owner amineutron).
- Services systemd durcis :
ProtectSystem=strict, ProtectHome=read-only avec le seul etat
en ecriture, PrivateTmp, SystemCallFilter=@system-service, CapabilityBoundingSet= vide,
UMask=0077. Verifiable avec systemd-analyze security tracking-api.service.
- Limite connue :
lyra-daemon garde un sudo NOPASSWD pour piloter la machine (services,
VMs, audio). Ce n'est pas le tracking qui l'accorde, et le durcir releve du projet Lyra ; tant
que ce daemon existe, un attaquant qui obtiendrait l'execution de code sous cet utilisateur
disposerait de ce pouvoir, independamment des protections ci-dessus.
- n8n restreint a
127.0.0.1:5678 dans docker-compose.yml
dv_webhook_server.py ecoute sur 0.0.0.0:8787 (necessaire pour recevoir les webhooks Docker) -- proteger ce port avec un firewall si la machine est exposee
- Les services systemd tournent avec
NoNewPrivileges=true
- Aucun secret en clair dans le code :
poller.py lit $CREDENTIALS_DIRECTORY (service user) ou dechiffre credentials/*.cred via systemd-creds decrypt --user (service systeme), avec repli sur les variables QBT_PASSWORD / BAZARR_KEY pour le debug
Ajouter un template
- Ouvrir
templates.py et ajouter une entree dans TEMPLATES :
- Optionnel : ajouter une simulation
_sim_mon_template() dans sim.py.
Le template est immediatement disponible sans autre modification.
Sans toucher au code, un template peut aussi etre declare dans ~/.config/tracking/templates.json
(meme structure, cle = nom du template) ; il est charge au demarrage.
Tests
La fixture autouse de conftest.py redirige la persistence vers un tmp_path : les tests ne
touchent jamais l'etat de production.
Part of the Lyra ecosystem