# Ma montre parle à Claude, sous conditions

Langue : fr

Identifiant : article:serveur-mcp-garmin

Noa Matout

J'ai écrit un serveur MCP pour interroger mes données d'entraînement Garmin. Les vraies données ont cassé trois choses que mes tests ne voyaient pas.

Garmin Connect garde des années de mes entraînements et ne sait répondre qu'aux
questions que ses concepteurs ont prévues. Pour le reste, il faut exporter des
fichiers et ouvrir un tableur.

J'ai écrit un serveur MCP qui récupère les fichiers FIT d'origine, les lit, les
range dans une base DuckDB locale et expose 16 outils
typés à Claude. Je peux lui demander de comparer mes deux dernières sorties, ou
de vérifier si ma semaine a suivi le plan. Tout tourne chez moi : aucun service
tiers ne garde mes données. [Consulter le code de mon serveur pour les données sportives](https://github.com/NoaMatout/garmin-mcp).

Ce qui m'intéresse ici n'est pas la fonction. C'est ce qu'il faut refuser à un
modèle pour qu'il puisse toucher à des données personnelles, et ce que les
vraies données m'ont appris que mes tests ignoraient.

## Pas de SQL libre

Donner à un modèle un accès SQL arbitraire sur une base personnelle, c'est lui
donner tout ce que le fichier contient. Il peut être convaincu de tout lire, et
il écrira parfois une requête qui parcourt un million de lignes pour parler de
mardi dernier.

Chaque requête du serveur est écrite à l'avance et paramétrée, dans un seul
module. Les noms de champs qu'un modèle demande passent par une liste
d'autorisation, parce qu'ils viennent du modèle. Je n'ai qu'un fichier à
relire, plutôt qu'une promesse à croire.

## Écrire sur ma montre demande deux fois mon accord

Lire un historique et modifier un compte sont deux actes différents. Le second
arrive à mon poignet.

L'écriture est désactivée par défaut. Cloner le dépôt ne donne pas à un modèle
le droit de modifier un compte Garmin. Une fois activée, aucune écriture ne se
fait en un seul appel. Le premier appel valide la séance en local, n'envoie
rien et la renvoie en entier. Seul un second appel, avec une confirmation
explicite, la crée. Pendant l'aperçu, aucun identifiant n'est même chargé :
c'est ce qui rend la confirmation réelle.

Chaque création a son annulation, et l'outil qui liste les séances existe pour
que cette annulation soit atteignable. Une séance enregistrée n'est pas une
séance planifiée : la mettre au calendrier est un autre outil, avec sa propre
confirmation. C'est la règle que j'applique à mes agents, décrite dans
[l'article sur ce que les administrateurs acceptent](/fr/articles/agent-que-les-sysadmins-accepteraient/) :
il propose, je décide.

## Le serveur ne sait pas se connecter

Les composants qui parlent à Garmin sont construits sans identifiants. Ils ne
peuvent que reprendre une session déjà ouverte. Seule une commande lancée à la
main voit un mot de passe, et le mot de passe n'est jamais écrit sur le disque.

Conséquence voulue : quand la session Garmin expire, le serveur ne reste pas
bloqué sur une demande de double authentification que personne ne verra. Mon
historique s'arrête simplement à la dernière synchronisation, et le serveur
continue de répondre.

## Quand Garmin a fermé la porte

En mars 2026, Garmin s'est mis à filtrer les clients selon la forme de leur
connexion TLS. Les clients HTTP ordinaires ont cessé de fonctionner, et la
bibliothèque que je comptais utiliser a été abandonnée.

J'en ai tiré une règle : l'import manuel n'est pas un plan de secours, c'est un
pilier. Je dépose des fichiers FIT exportés dans un dossier, une commande les
importe, sans réseau ni identifiant. C'est le seul chemin dont je peux promettre
qu'il marchera encore dans un an. Pour le reste, deux méthodes d'accès sont
cachées derrière une interface étroite, et une troisième pourra s'y brancher le
jour où Garmin rouvrira une API officielle.

## Une moyenne qui mentait sur ma fréquence cardiaque

Une sortie de trois heures contient des milliers de points par mesure. Pour ne
pas noyer le modèle, le serveur les regroupe et en fait la moyenne. Sur une
sortie réelle de 1 513 échantillons, un aperçu à
10 points annonçait une fréquence cardiaque
maximale de 160,2. La vraie valeur était
174. Le minimum était pire :
138 annoncé, 111 en
réalité.

Un aperçu grossier est exactement ce qu'un modèle demande quand il veut une
allure générale. Ce n'était donc pas un cas limite, c'était le chemin courant.
Et « votre fréquence maximale était de 160,2 » est une réponse fausse, donnée avec
assurance, sur un chiffre que les sportifs prennent au sérieux.

Chaque réponse porte désormais les vrais extrêmes, calculés sur tous les
échantillons, à côté de la courbe lissée.

## Une charge d'entraînement qui n'existait pas

Le résumé hebdomadaire additionnait un score Garmin qui décrit une seule séance,
noté de un à cinq, et appelait le total « charge d'entraînement ». Sur une vraie
semaine de six séances dans
quatre sports, il affichait
17. Ce nombre n'a aucun sens, et il a l'air
sérieux.

Un journal d'entraînement est précisément l'endroit où ce genre de chiffre fait
des dégâts. Je l'ai supprimé plutôt que renommé.

## Des titres effacés sans une erreur

En regardant les données plutôt que le code, j'ai vu
66 activités Garmin et
un seul titre. La commande qui relit les
fichiers stockés avait effacé les autres, sans aucune erreur.

Les titres se saisissent dans Garmin Connect et arrivent avec la liste des
activités. Ils ne sont pas dans le fichier FIT. Reconstruire une ligne à partir
du seul fichier perdait donc tout ce que le fichier ne porte pas. Une commande
dont le rôle est de relire les fichiers stockés était certaine de les perdre.

La relecture conserve maintenant les titres existants, et chaque
synchronisation les rafraîchit. La correction a rendu
91 titres et révélé
25 activités plus anciennes qui n'avaient
jamais été importées, parce que la liste lue chez Garmin était tronquée.

Aucune de ces trois pannes n'était visible dans la suite de tests. Toutes
sont apparues en regardant de vraies séances.

## Ce que j'ai fait pour les voir plus tôt

Le lecteur vise le format FIT, pas une montre. Je le valide sur
42 enregistrements réels venant de
19 appareils et
huit fabricants. Parmi eux,
30 sont lus comme des activités. Les
12 autres sont refusés à raison : réglages,
pesées, suivi quotidien, ou un fichier tronqué avant sa première séance.

Les vrais appareils ont des manies qu'aucun fichier synthétique n'invente : une
montre qui enregistre avant le départ, une fréquence cardiaque à zéro au lieu de
« absente », une heure de départ illisible. Chaque valeur reconstituée porte une
marque, pour qu'un chiffre déduit ne passe jamais pour un chiffre mesuré.

La suite hermétique, sans données ni réseau, compte
214 tests réussis. Je l'ai relancée avant
d'écrire cet article.

## Ce que je n'ai pas pu vérifier

L'accès automatique à Garmin repose sur des méthodes non officielles. Il a déjà
cassé une fois, et il cassera encore. Seul l'import manuel est garanti.

Le corpus couvre 19 appareils, pas tous ceux du
marché, et l'essentiel de mon usage porte sur mes propres données.

Enfin, j'avais soigné le code et oublié le système. Lancé sans aucun
confinement, ce même serveur obtenait une note d'exposition de
9,2 sur 10. Je
raconte comment je l'ai mesurée et corrigée dans
[l'article sur l'audit de mon propre serveur MCP](/fr/articles/auditer-ses-propres-mcp/).

## Sources

- [Forum Garmin, « TLS fingerprinting blocks third-party clients »](https://forums.garmin.com/apps-software/mobile-apps-web/f/garmin-connect-web/433892/tls-fingerprinting-blocks-third-party-clients/2022045),
  pour le blocage des clients non navigateurs en 2026.

Les mesures sont les miennes, tirées du dépôt, de son historique et de sa suite
de tests.

## Périmètre de la mesure

tools : 16 outils typés. Outils exposés par le serveur MCP, sans aucun accès SQL libre.

testsPassed : 214 tests réussis. Suite hermétique, sans données réelles ni réseau, relancée le 2 octobre 2026.

corpusFiles : 42 enregistrements réels. Fichiers FIT réels utilisés pour valider le lecteur, hors de la suite hermétique.

corpusDevices : 19 appareils. Appareils différents représentés dans le corpus de validation.

corpusMakers : 8 fabricants. Fabricants représentés dans le corpus de validation.

corpusParsed : 30 fichiers lus. Fichiers du corpus lus comme des activités.

corpusRejected : 12 rejets corrects. Fichiers du corpus qui ne sont pas des activités ou sont tronqués, et que le lecteur refuse à raison.

streamSamples : 1 513 échantillons. Points de fréquence cardiaque d'une sortie réelle, utilisés pour mesurer l'effet du sous-échantillonnage.

overviewPoints : 10 points. Résolution d'un aperçu grossier de la même sortie.

hrMaxRead : 160,2 battements par minute. Fréquence cardiaque maximale lue dans l'aperçu à dix points, avant correction.

hrMaxTrue : 174 battements par minute. Fréquence cardiaque maximale réelle de la même sortie.

hrMinRead : 138 battements par minute. Fréquence cardiaque minimale lue dans l'aperçu à dix points, avant correction.

hrMinTrue : 111 battements par minute. Fréquence cardiaque minimale réelle de la même sortie.

loadSessions : 6 séances. Séances d'une semaine réelle additionnées par l'ancien résumé hebdomadaire.

loadSports : 4 sports. Sports représentés dans cette semaine.

loadValue : 17 points de charge. Somme sans signification produite par l'ancien résumé, affichée comme une charge d'entraînement.

titledActivities : 66 activités. Activités Garmin présentes dans ma base au moment où j'ai remarqué la perte des titres.

survivingTitles : 1 titre. Titre encore présent après la relecture complète des fichiers.

recoveredTitles : 91 titres. Titres récupérés après correction, en élargissant la liste lue chez Garmin.

missedActivities : 25 activités. Activités plus anciennes jamais importées, découvertes pendant la même correction.

exposureBefore : 9,2 sur 10. Note d'exposition de mon serveur MCP avant durcissement, relevée par l'outil d'analyse de systemd. Plus elle est basse, mieux c'est.
