Skip to main content
Glama

Gamin-mcp-webui

Le serveur MCP garmin-mcp et une interface web de consultation des comptes, dans un seul dépôt et une seule image Docker.

Le serveur amont, écrit en Go, authentifie plusieurs comptes via OAuth 2.1 en mode remote et garde leur état dans une base SQLite. Il n'expose aucune page ni API d'administration : impossible de savoir, depuis le serveur lui-même, qui a créé un compte ni quand ce compte s'est connecté pour la dernière fois. Ce dépôt ajoute cette vue et fait tourner les deux ensemble.

Le serveur amont est vendorisé dans garmin-mcp/ par git subtree : c'est une copie versionnée du dépôt public, mettable à jour d'une commande (voir « Suivre l'amont »), et non un simple téléchargement au moment du build.

Capture de l'interface : tuiles d'indicateurs et tableau des comptes avec leur dernière connexion

              conteneur unique
┌───────────────────────────────────────────────┐
│  garmin-mcp serve  ──écrit──►  /data/garmin.db│  :8180  endpoint MCP + login OAuth
│  (Go, OAuth 2.1)               (SQLite, WAL)  │
│                                    ▲   ▲      │
│  interface web  ────lit (RO)───────┘   │      │  :8080  interface + API JSON
│  (FastAPI)      ────valide────────────►┘      │           (écrit account_approvals)
└───────────────────────────────────────────────┘
                    /data : base + clé maîtresse + TLS (volume)

Les deux processus tournent sous le même compte de service, condition nécessaire pour qu'une base SQLite en mode WAL soit lisible par le second. Le conteneur s'arrête dès que l'un des deux s'arrête, pour que la politique de redémarrage de Docker s'applique à l'ensemble.

Ce que l'interface affiche

Colonne

Origine

Compte

principals.email_normalized — l'e-mail est l'identifiant de connexion, la seule donnée en clair du compte.

Garmin lié

présence de principals.garmin_account_hash.

Compte créé le

principals.created_at.

Dernière connexion

dérivée (voir ci-dessous).

Signal retenu

le fait horodaté qui porte la date affichée.

Clients / familles

consents non révoqués et token_families actives.

Notice acceptée

dernière ligne de privacy_notice_consents (voir plus bas).

Validation

account_approvals : en attente, validé ou bloqué (voir plus bas).

Le détail d'un compte ajoute la liste complète des signaux, ses acceptations de la notice de confidentialité, les clients OAuth autorisés avec leurs portées, les familles de jetons et, si la table est alimentée, les événements d'audit.

Related MCP server: mcp-server-garmin

Comment la « dernière connexion » est calculée

Le schéma de garmin-mcp n'a pas de colonne « dernière connexion ». Il stocke des faits horodatés ; l'interface prend le plus récent d'entre eux et indique lequel a gagné.

Signal

Source

Ce qu'il prouve

Rafraîchissement des jetons Garmin

garmin_token_sets.updated_at

usage réel : les jetons Garmin sont réécrits à chaque rafraîchissement.

Émission d'un jeton MCP

max(mcp_tokens.issued_at) via token_families

le client MCP a obtenu un jeton d'accès ou l'a fait tourner.

Autorisation dans le navigateur

max(auth_codes.created_at)

passage complet par la page de login.

Consentement accordé

max(consents.granted_at)

consentement donné à un client MCP.

Événement d'audit

max(audit_events.occurred_at)

table présente dans le schéma, sans écrivain dans les versions 0.0.x.

Mise à jour du compte

principals.updated_at

repli, retenu seulement s'il est postérieur à created_at.

Un compte dont aucun signal ne dépasse sa date de création est classé jamais connecté — updated_at vaut alors created_at et ne prouve aucune connexion.

États dérivés (seuils configurables) : actif ≤ 7 j, inactif ≤ 30 j, dormant au-delà, jamais connecté sans aucun signal.

Limites à connaître

  • Rétention. La tâche de nettoyage de garmin-mcp supprime les codes et les jetons expirés. Un compte inactif depuis longtemps peut donc voir sa dernière connexion « reculer » sur un signal plus ancien mais persistant (consentement, jetons Garmin). Les consentements, eux, ne sont jamais supprimés par le nettoyage.

  • Rien n'est instrumenté côté requête. garmin-mcp n'écrit pas d'horodatage de dernier appel d'outil ; la granularité est donc celle de l'émission des jetons, pas celle de chaque requête MCP. Pour une vraie mesure d'usage, il faudrait un correctif amont alimentant audit_events — cette interface l'affichera automatiquement si la table se remplit.

  • Identité Garmin. Le nom Garmin du compte est chiffré dans la base (garmin_identity_sealed) et n'est déchiffrable qu'avec la clé maître du serveur. L'interface ne la demande pas et n'affiche donc que l'e-mail.

Le guide destiné aux utilisateurs

docs/connecter-garmin-a-claude.md est la notice à donner aux personnes qu'on invite sur un déploiement : brancher le connecteur dans Claude, se connecter à Garmin, accepter la notice de confidentialité, puis attendre la validation de l'exploitant. Elle décrit aussi ce que le serveur enregistre et ce qu'il n'enregistre pas, en langage courant.

Elle est écrite pour ce déploiement : l'URL du serveur MCP et l'identifiant de client OAuth qu'elle donne sont ceux d'une installation particulière, à remplacer par les vôtres. Les captures des écrans de Claude sont à déposer dans docs/captures/, qui liste celles que le guide attend ; celles des deux pages servies par le serveur lui-même sont déjà là.

Le consentement de l'utilisateur

Ce dépôt ajoute au serveur amont une fenêtre de consentement : la page qui conclut le login dans le navigateur — celle qui porte Autoriser et Refuser — affiche désormais ce que le déploiement enregistre et ce qu'il n'enregistre pas, et l'acceptation est enregistrée en base.

La page de consentement, notice dépliée

  • Un résumé toujours visible : ce qui est enregistré, ce qui ne l'est jamais, ce que reçoit le client MCP, et comment arrêter le traitement.

  • Le texte intégral à la demande, dans un dépliant sur la même page — aucun second chargement, donc rien qui puisse échouer entre la lecture et l'acceptation.

  • Une case à cocher que le serveur vérifie. Accorder sans cocher est refusé côté serveur, pas seulement par le navigateur : la page revient, la session reste vivante, rien n'a été accordé. Refuser, en revanche, n'exige aucune acceptation.

  • On ne redemande pas. Qui a déjà accepté le texte servi voit la date de son acceptation à la place de la case.

Ce qui est enregistré, et pourquoi c'est l'empreinte qui compte

Une ligne par compte et par texte accepté, dans la table privacy_notice_consents (migration 0003 du serveur) : l'identifiant interne du compte, l'empreinte SHA-256 du texte exact affiché, le libellé de version que portait ce texte, et l'instant de la première acceptation.

C'est l'empreinte, et non le libellé, qui décide si l'on redemande. Un libellé s'oublie au moment d'éditer le texte ; une empreinte non. Modifier la notice — un mot, un titre, ou sa traduction complète en français — change l'empreinte, ne correspond plus à aucune ligne, et fait donc réaccepter tout le monde, sans qu'aucune action d'exploitation ne soit nécessaire.

Réaccepter le même texte garde le premier instant : c'est celui-là qui a eu lieu. Accepter un texte différent ajoute une ligne au lieu d'en remplacer une, donc l'historique de ce qui a été accepté survit.

Modifier le texte

Il est dans garmin-mcp/internal/loginweb/pages/remote/privacy.html, en deux blocs (résumé et texte intégral), en français. Le modifier ou le traduire est une édition de ce seul fichier ; l'empreinte changeant, chacun le réacceptera. Pensez à remonter PrivacyNoticeVersion dans garmin-mcp/internal/loginweb/privacy.go pour que la ligne enregistrée porte aussi un libellé lisible. Le détail est dans garmin-mcp/docs/privacy-notice.md.

Tout le login distant est en français, pas seulement le bloc de notice : les sept pages de garmin-mcp/internal/loginweb/pages/remote/ — annonce, identifiants Garmin, code à usage unique, consentement, attente de validation, connexion expirée, page terminale — et les messages d'erreur que ces pages affichent. Le profil « loopback », celui de la commande garmin-mcp login que seul l'exploitant utilise depuis son terminal, reste en anglais comme en amont. C'est le prix à payer : chaque fichier traduit est un conflit potentiel à la prochaine mise à jour du subtree, ce qui est acceptable pour des pages que vos utilisateurs lisent et ne l'était pas pour un outil d'exploitation.

Le texte livré décrit ce que ce build fait réellement, vérifié contre le schéma. Si vous changez ce que le serveur stocke, la notice fait partie du changement — et c'est l'opérateur du déploiement qui reste responsable du traitement et de ce que la notice promet.

La page qui clôt le login

Un login réussi se termine par une redirection vers le client : la dernière page que la personne voit est celle de Claude, pas celle du serveur. La transaction est alors close et son cookie effacé — c'est voulu, une transaction terminée ne doit plus être adressable.

Conséquence : toute requête ultérieure sur une route /login… tombe sur une page terminale, et c'est le cas normal quand la fenêtre d'autorisation est rouverte, rechargée, ou revisitée avec le bouton « précédent » après coup. Cette page dit maintenant ce qui s'est passé — « Si vous venez d'autoriser l'accès, tout s'est bien passé » — au lieu du Nothing here amont, qui se lisait comme une panne alors que le connecteur venait de se connecter.

Seules /authorize et les quatre routes /login… servent cette page. Une adresse inconnue du déploiement obtient le 404 nu du serveur HTTP, sans mise en forme : si quelqu'un vous montre une page blanche portant 404 page not found, il n'était pas dans le login.

Côté interface

L'interface montre, par compte, la date de la dernière acceptation et sa version, ou une pastille Aucun pour un compte qui n'a jamais accepté de notice — l'état normal d'un compte antérieur à la mise en place. Deux tuiles comptent les deux populations, un filtre isole les comptes sans consentement, le détail liste toutes les acceptations avec leur empreinte, et l'export CSV porte les mêmes colonnes.

Écrire les séances dans Garmin

Par défaut, un déploiement est en lecture seule. Pour que Claude puisse créer, envoyer et planifier des séances dans le compte Garmin de la personne, deux choses doivent être vraies en même temps :

  1. GARMIN_MCP_ENABLE_WRITE_TOOLS=true — sinon les outils d'écriture ne sont pas servis du tout ;

  2. le client OAuth déclare la portée garmin:write, et la personne l'accorde sur la page de consentement, qui la lui affiche nommément.

GARMIN_MCP_ENABLE_WRITE_TOOLS=true
GARMIN_MCP_OAUTH_CLIENTS=[{"id":"claude-web-desktop","name":"Claude",
  "redirect-uris":["http://127.0.0.1:33418/callback"],
  "scopes":["garmin:read","garmin:write"],
  "resources":["https://mcp.exemple.fr/mcp"],"public":true}]

Les deux conditions sont nécessaires, et c'est ce qui rend le réglage sûr : activer les outils ne donne rien tant que personne n'a consenti, et consentir ne donne rien si le serveur ne les sert pas.

Le nom de la portée est un piège. Seuls garmin:write et garmin:destructive commandent les paliers (internal/policy/tier.go). Le manifeste amont compat/tools.json, lui, nomme la portée par outil garmin:workouts:write — un nom plausible, accepté partout, et qui n'autorise rien : la configuration l'accepte, le serveur l'annonce dans scopes_supported, /authorize la délivre, le jeton la porte, et la politique la refuse en silence, outil par outil. Le symptôme est un déploiement où server_info affiche la portée accordée et un palier resté read-only. e2e/writescope_test.go compare les deux jetons et fige le bon nom.

La portée est large. garmin:write ouvre le palier d'écriture entier — 37 outils, pas seulement les séances. Dix concernent les séances (create_run_workout, create_strength_workout, create_walk_run_workout, create_z2_walk_workout, upload_workout, upload_workouts, update_workout, schedule_workout, schedule_workouts, schedule_week) ; les 27 autres touchent le poids, la nutrition, l'hydratation, les métadonnées d'activité, le matériel et les zones cardiaques. Le compte passe de 108 outils visibles à 145.

Pour n'ouvrir que les séances, la portée ne suffit pas — il faut retirer les autres nommément, avec GARMIN_MCP_TOOL_DENYLIST (réglage amont ; voir garmin-mcp/docs/configuration.md) :

GARMIN_MCP_TOOL_DENYLIST=add_body_composition,add_gear_to_activity,add_hydration_data,add_weigh_in,add_weigh_in_with_timestamps,create_custom_food,create_manual_activity,create_strength_training_activity,download_activity_file,download_course_gpx,log_custom_food,log_food,remove_gear_from_activity,request_reload,set_activity_description,set_activity_event_type,set_activity_feel,set_activity_name,set_activity_strength_exercise_sets,set_activity_type,set_blood_pressure,set_heart_rate_zones,set_nutrition_daily_settings,set_perceived_effort,update_custom_food,upload_course,upsert_and_log

Une liste à tenir à jour : un outil d'écriture ajouté par l'amont y sera absent, donc autorisé. Le réglage par outil de claude.ai (« Nécessite une approbation », capture 07) est plus souple mais reste côté client — c'est un garde-fou, pas une barrière.

Vérifier les deux moitiés, sans deviner. doctor ne connaît que la première — ce que l'exploitant a activé :

$ docker compose exec garmin-mcp garmin-mcp doctor | grep -A3 "tool tiers"
tool tiers:
  write: enabled
  destructive: disabled

La seconde — ce que le jeton de la personne autorise réellement — se lit dans l'outil server_info, depuis Claude, et c'est le seul endroit où les deux moitiés se rencontrent :

Champ

Lecture seule

Écriture effective

Paliers activés

read-only

read-only, write

Scopes accordés

garmin:read

garmin:read, garmin:write

Outils visibles

108

145

Un palier resté read-only alors que la portée d'écriture apparaît dans les scopes accordés signale l'une des deux causes : GARMIN_MCP_ENABLE_WRITE_TOOLS n'est pas pris en compte, ou la portée accordée n'est pas garmin:write.

Supprimer reste impossible. delete_workout et unschedule_workout sont classés destructifs : ils demandent GARMIN_MCP_ENABLE_DESTRUCTIVE_TOOLS=true et la portée garmin:destructive, ni l'un ni l'autre activés ici. Les deux portées sont indépendantes : garmin:write n'implique pas garmin:destructive.

Changer les portées d'un client est une ré-autorisation : les jetons existants ont été émis pour les anciennes, et chaque personne devra repasser par la page de consentement. La notice de confidentialité, elle, n'a pas à changer — elle dit déjà que le client reçoit un jeton « limité aux permissions affichées sur cette page », ce qui reste exact quelles que soient ces permissions.

La validation des comptes

Par défaut, un nouveau compte n'est utilisable qu'une fois validé dans l'interface. Quelqu'un qui se connecte avec ses identifiants Garmin obtient un compte, voit une page qui le lui dit, et n'obtient rien d'autre : aucun consentement enregistré, aucun code d'autorisation, aucun jeton.

La page qu'un compte non validé obtient à la fin du login

Où

Ce qui se passe

Page de consentement

un compte non validé y accède normalement : un bandeau annonce l'attente, et il peut accepter la notice dès maintenant. Son acceptation est enregistrée.

Au clic sur Autoriser

l'acceptation est écrite, puis la porte s'applique : compte non validé → page « en attente », transaction OAuth close, aucun jeton.

À chaque requête MCP

la lecture du jeton d'accès refuse un compte non validé. Retirer une validation coupe l'accès à la requête suivante, pas au prochain login.

Dans l'interface

colonne Validation, filtre, tuile, et les boutons Valider / Bloquer / Remettre en attente.

L'ordre compte : accepter la notice est la décision de la personne sur ses propres données, et elle n'a pas à attendre la décision de l'exploitant sur son accès. Une fois validée, la personne relance l'autorisation depuis son client et la notice ne lui est pas redemandée.

Trois états : en attente (personne n'a décidé), validé, bloqué. « En attente » n'est jamais stocké — c'est l'absence de ligne dans account_approvals, si bien qu'un compte tout juste créé attend par construction, sans que rien n'ait eu à s'exécuter. La migration 0004 valide en revanche tous les comptes qui existaient déjà : activer la porte sur un déploiement en cours ne met personne dehors.

Le réglage GARMIN_MCP_REQUIRE_ACCOUNT_APPROVAL (défaut true) commande la porte. À false, on retrouve le comportement amont : un compte est utilisable dès que son login Garmin a réussi.

Être prévenu par e-mail

Rien n'annonce un compte retenu : sans notification, on l'apprend quand la personne se plaint. Renseignez un serveur SMTP et le déploiement envoie un message par compte mis en attente.

Variable

Rôle

GARMIN_MCP_SMTP_HOST

le serveur. Vide n'envoie rien, c'est le défaut.

GARMIN_MCP_SMTP_PORT

587 en STARTTLS (défaut), 465 en TLS implicite.

GARMIN_MCP_SMTP_USER

le compte d'envoi. Avec Gmail, l'adresse complète.

GARMIN_MCP_SMTP_SECRET_FILE

fichier contenant le secret, lisible par le seul propriétaire. Avec Gmail, un mot de passe d'application.

GARMIN_MCP_SMTP_FROM

l'expéditeur. Vide reprend SMTP_USER, ce que Gmail impose de toute façon.

GARMIN_MCP_SMTP_TO

les destinataires, séparés par des virgules.

GARMIN_MCP_SMTP_TLS

starttls ou implicit. Il n'y a pas de mode en clair.

GARMIN_MCP_DASHBOARD_URL

lien vers l'interface, placé dans le message.

Le secret n'est pas une variable d'environnement, et ce n'est pas un oubli : le serveur amont refuse par principe qu'un identifiant soit configurable — un test le vérifie — donc seul le chemin d'un fichier l'est, comme pour la clé maîtresse. Le fichier doit appartenir au compte de service et n'être lisible que par lui, sinon le démarrage échoue. Avec Gmail, générez un mot de passe d'application (la validation en deux étapes doit être active sur le compte) et écrivez-le seul dans ce fichier.

Trois garanties, chacune couverte par un test :

  • Un envoi ne retarde ni ne fait échouer un login. Il part en tâche de fond, sur un contexte détaché de la requête, et ses erreurs sont journalisées puis abandonnées.

  • Un compte n'est annoncé qu'une fois par intervalle (six heures). Trois tentatives de connexion font un seul message ; un retour une semaine plus tard fait un rappel. Un envoi qui échoue ne consomme pas l'intervalle.

  • Une configuration incomplète refuse de démarrer. Nommer un serveur et oublier les destinataires est une erreur qu'on ne découvrirait qu'en ne recevant rien.

Le message contient l'adresse e-mail du compte, son identifiant interne et l'heure — aucune donnée Garmin. La notice de confidentialité le dit, puisque cette adresse transite alors par un tiers que vous avez choisi.

L'interface écrit, mais une seule table

C'est la seule exception à la lecture seule, et elle n'est pas une convention de code : la connexion d'écriture installe un autorisateur SQLite qui refuse, avant exécution, toute écriture ailleurs que dans account_approvals et toute modification de schéma. Un test le vérifie en essayant huit requêtes interdites.

La route d'écriture est POST /api/accounts/{id}/approval, protégée en plus contre une écriture déclenchée depuis un autre site : elle n'accepte que du JSON et refuse un en-tête Origin qui ne désigne pas l'interface — nécessaire parce que le navigateur rejoue tout seul l'authentification HTTP Basic.

Si l'interface tourne séparément avec le volume monté en lecture seule, la validation est impossible : la route répond 409 en le disant, et la consultation continue de marcher.

Sécurité

Ce qui suit concerne l'interface web. Le modèle de menace du serveur MCP lui-même — isolation des comptes, chiffrement des jetons, gestion des clés — est celui du projet amont, décrit dans garmin-mcp/docs/threat-model.md.

  • Lecture seule, sauf une table. Les connexions de consultation sont ouvertes en mode=ro avec PRAGMA query_only, et un test vérifie qu'un DELETE échoue. La seule écriture de toute l'interface est la validation des comptes, bornée à account_approvals par un autorisateur SQLite — voir « La validation des comptes ».

  • Conteneur non privilégié. L'entrypoint n'est root que le temps d'ajuster l'appartenance de /data, puis redescend sur un compte de service via setpriv. La racine du système de fichiers peut rester en lecture seule (read_only: true dans le compose), /data est en 0700, et les deux ports servis sont publiés sur la boucle locale.

  • Deux ports, deux publics. 8180 est l'endpoint MCP et les pages de login OAuth, destinés aux utilisateurs ; 8080 est l'interface d'exploitation, qui affiche leurs adresses e-mail. Ne les exposez pas au même public.

  • Aucun secret ne sort. Les colonnes *_hash (empreintes) et *_sealed (enveloppes chiffrées) ne sont jamais sélectionnées ni renvoyées ; un test parcourt les réponses de l'API pour s'en assurer.

  • Authentification obligatoire. Le service refuse de démarrer sans WEBUI_PASSWORD ni WEBUI_API_TOKEN, sauf WEBUI_ALLOW_ANONYMOUS=1 assumé explicitement. Les comparaisons de secrets passent par secrets.compare_digest.

  • Données personnelles. L'interface affiche des adresses e-mail : exposez-la sur une adresse privée, derrière un reverse proxy en TLS, et activez WEBUI_MASK_EMAILS=1 si un affichage partiel suffit. La page envoie noindex, nofollow.

Démarrage rapide

cp .env.example .env      # renseignez au minimum WEBUI_PASSWORD et l'URL publique
docker compose up -d --build

L'image compile le serveur Go depuis garmin-mcp/ puis l'embarque avec l'interface web. L'interface écoute sur 127.0.0.1:8080, l'endpoint MCP sur 127.0.0.1:8180. Au premier démarrage, le serveur crée sa clé maîtresse, migre sa base et commence à servir ; l'interface affiche « base introuvable » les quelques secondes qui précèdent.

Essai sur un poste, sans reverse proxy

Le serveur d'autorisation refuse de nommer un émetteur en clair : l'URL publique doit être https, et aucun override ne change cela (allow-insecure-http ne lève que le contrôle sur l'écoute et l'origine). Pour un essai local, laissez le conteneur terminer le TLS avec un certificat auto-signé :

GARMIN_MCP_SELF_SIGNED_TLS=1 \
GARMIN_MCP_PUBLIC_URL=https://127.0.0.1:8180/mcp \
GARMIN_MCP_BIND_ADDRESS=127.0.0.1:8180 \
GARMIN_MCP_OAUTH_CLIENTS='[{"id":"claude-desktop","name":"Claude Desktop","redirect-uris":["http://127.0.0.1:33418/callback"],"scopes":["garmin:read"],"resources":["https://127.0.0.1:8180/mcp"],"public":true}]' \
WEBUI_PASSWORD=demo \
docker compose up --build

Le certificat est écrit une fois dans /data/tls et n'est jamais remplacé. Il est fait pour un essai, pas pour une mise en production : un client MCP refusera une autorité inconnue.

En production

Mettez un reverse proxy TLS devant, et donnez au serveur l'URL publique de ce proxy :

Réglage

Valeur

GARMIN_MCP_PUBLIC_URL

https://mcp.exemple.fr/mcp — l'URL que voient les clients.

GARMIN_MCP_BIND_ADDRESS

0.0.0.0:8180, avec GARMIN_MCP_ALLOW_INSECURE_HTTP=true puisque le proxy termine le TLS.

GARMIN_MCP_TRUSTED_PROXY_CIDRS

le réseau du proxy, sans quoi aucun en-tête X-Forwarded-* n'est cru.

GARMIN_MCP_OAUTH_CLIENTS

au moins un client ; il n'y a pas d'enregistrement dynamique.

Alternative sans proxy : montez vos propres GARMIN_MCP_TLS_CERT_FILE et GARMIN_MCP_TLS_KEY_FILE, le serveur termine alors le TLS lui-même.

Sauvegarde. La base et la clé maîtresse sont les deux moitiés d'une même sauvegarde : une base sans sa clé est illisible. Sauvegardez /data en entier, avec le processus arrêté ou via la sauvegarde en ligne de SQLite. Voir garmin-mcp/docs/operations.md.

Ne lancer qu'un service

RUN_SERVICES vaut les-deux (défaut), mcp ou webui. Deux conteneurs issus de la même image, l'un en mcp et l'autre en webui sur le même volume, sont une configuration valide — le second bascule alors sur une copie temporaire de la base si le volume lui est monté en lecture seule, et la validation des comptes n'est alors pas possible depuis ce conteneur.

En local, sans conteneur

python -m venv .venv && source .venv/bin/activate
pip install -r requirements-dev.txt

# Pour essayer l'interface sans base de production :
python scripts/demo_database.py /tmp/demo.db
WEBUI_DATABASE_PATH=/tmp/demo.db WEBUI_PASSWORD=demo python -m app
# http://127.0.0.1:8080 — identifiants : admin / demo

Le serveur MCP seul se compile comme n'importe quel programme Go :

cd garmin-mcp && go build ./cmd/garmin-mcp && ./garmin-mcp tools list | grep ' tools:'

Configuration

Serveur MCP

Chaque réglage de garmin-mcp a une variable d'environnement : la clé en majuscules, tirets remplacés par des soulignés, préfixée GARMIN_MCP_. La liste complète est dans garmin-mcp/docs/configuration.md. Les principales pour cette image :

Variable

Défaut dans l'image

Rôle

GARMIN_MCP_PUBLIC_URL

—

Obligatoire. URL publique de l'endpoint MCP. Doit être https.

GARMIN_MCP_OAUTH_CLIENTS

—

Obligatoire. Registre des clients OAuth, en JSON.

GARMIN_MCP_BIND_ADDRESS

0.0.0.0:8180

écoute dans le conteneur.

GARMIN_MCP_ALLOW_INSECURE_HTTP

false

autorise une écoute et une origine en clair hors boucle locale. Ne rend jamais un émetteur en clair acceptable.

GARMIN_MCP_TRUSTED_PROXY_CIDRS

vide

réseaux dont les en-têtes X-Forwarded-* sont crus.

GARMIN_MCP_TLS_CERT_FILE / _KEY_FILE

—

le serveur termine le TLS lui-même.

GARMIN_MCP_SELF_SIGNED_TLS

0

ajout de cette image : fabrique un certificat auto-signé dans /data/tls pour un essai local.

GARMIN_MCP_DATABASE_PATH

/data/garmin.db

base SQLite, partagée avec l'interface.

GARMIN_MCP_MASTER_KEY_FILE

/data/keys/key-v1.json

la valeur sélectionne le répertoire ; le nom de fichier appartient au serveur.

GARMIN_MCP_STATE_DIR

/data

état hors base.

GARMIN_MCP_ENABLE_WRITE_TOOLS

false

sert les outils d'écriture. Sans portée accordée, ils restent refusés — voir « Écrire les séances ».

GARMIN_MCP_ENABLE_DESTRUCTIVE_TOOLS

false

sert les outils de suppression. Exige aussi ENABLE_WRITE_TOOLS et une portée :destructive.

GARMIN_MCP_REQUIRE_ACCOUNT_APPROVAL

true

ajout de ce dépôt : un nouveau compte attend une validation dans l'interface.

Conteneur

Variable

Défaut

Rôle

RUN_SERVICES

les-deux

les-deux, mcp ou webui : ce que l'entrypoint lance.

APP_USER

webui

compte de service auquel l'entrypoint redescend après avoir ajusté /data.

Interface web

Toutes les variables sont facultatives sauf le secret d'accès.

Variable

Défaut

Rôle

WEBUI_DATABASE_PATH

/data/garmin.db

base SQLite de garmin-mcp (sa clé database-path).

WEBUI_USERNAME

admin

identifiant HTTP Basic.

WEBUI_PASSWORD

—

mot de passe HTTP Basic. Obligatoire (voir WEBUI_ALLOW_ANONYMOUS).

WEBUI_API_TOKEN

—

jeton porteur pour l'API JSON (Authorization: Bearer …).

WEBUI_ALLOW_ANONYMOUS

0

ouvre l'interface sans authentification. À réserver à un proxy qui authentifie déjà.

WEBUI_MASK_EMAILS

0

masque les e-mails (a***@e***.fr) dans l'interface et les exports.

WEBUI_ACTIVE_DAYS

7

seuil « actif », en jours.

WEBUI_IDLE_DAYS

30

seuil « inactif », en jours. Doit dépasser WEBUI_ACTIVE_DAYS.

WEBUI_SNAPSHOT_TTL_SECONDS

5

durée de validité de la copie quand la lecture directe échoue.

WEBUI_TITLE

Comptes garmin-mcp

titre affiché.

WEBUI_HOST / WEBUI_PORT

0.0.0.0 / 8080

écoute HTTP.

WEBUI_LOG_LEVEL

info

niveau de log uvicorn.

API

Toutes les routes /api sauf /api/health exigent une authentification.

Route

Description

GET /

l'interface.

GET /api/health

sonde publique : {"status": "ok", "database_readable": true}.

GET /api/status

chemin, taille et mode d'accès de la base, seuils, libellés des signaux.

GET /api/stats

compteurs agrégés (total, liés à Garmin, actifs, vus sous 24 h…).

GET /api/accounts

liste paginée. Paramètres : search, status, linked, consent, sort, order, limit, offset.

GET /api/accounts.csv

même liste au format CSV, mêmes filtres.

GET /api/accounts/{id}

détail d'un compte : signaux, acceptations de la notice, consentements, familles de jetons, audit.

POST /api/accounts/{id}/approval

valide, bloque ou remet en attente. Corps JSON {"state": "approved"|"blocked"|"pending", "note": "…"}.

GET /api/docs

documentation OpenAPI générée.

curl -u admin:motdepasse 'http://127.0.0.1:8080/api/accounts?status=dormant'
curl -H "Authorization: Bearer $WEBUI_API_TOKEN" http://127.0.0.1:8080/api/stats

Développement

pip install -r requirements-dev.txt
ruff check . && ruff format --check .
pytest -q                       # interface web

cd garmin-mcp && go test ./...  # serveur MCP (suite amont)

shellcheck docker/entrypoint.sh
docker build -t gamin-mcp-webui:test .

La CI fait les quatre : lint et tests Python sur 3.11 et 3.12, compilation du serveur Go, shellcheck sur l'entrypoint, puis construction de l'image et démarrage réel du conteneur jusqu'à ce que sa sonde de santé passe.

Les tests construisent une base SQLite au schéma de garmin-mcp (tests/schema.sql) peuplée de cas limites : compte actif, inactif, dormant, jamais connecté, compte dont les jetons ont été purgés. tests/schema.sql reproduit les tables et colonnes lues par app/queries.py ; si le schéma amont évolue, c'est le fichier à mettre à jour — un test échouera alors immédiatement.

Suivre l'amont

garmin-mcp/ est un git subtree du dépôt public. Pour récupérer une version plus récente :

git remote add garmin-mcp-upstream https://github.com/tamcore/garmin-mcp.git   # une seule fois
git fetch garmin-mcp-upstream master
git subtree pull --prefix=garmin-mcp garmin-mcp-upstream master --squash

Le sous-répertoire reste modifiable comme le reste du dépôt ; git subtree push renvoie ces modifications vers un fork amont si vous en tenez un. Après une mise à jour, vérifiez que tests/schema.sql correspond toujours aux migrations amont — c'est ce que testent les tests de l'interface.

Compatibilité

Vérifié contre le schéma de garmin-mcp après les migrations 0001_initial, 0002_oauth_contract, 0003_privacy_notice_consent et 0004_account_approval (les deux dernières ajoutées par ce dépôt). L'interface ne dépend que des tables principals, garmin_token_sets, consents, oauth_clients, auth_codes, token_families, mcp_tokens, audit_events, privacy_notice_consents et account_approvals, et ne lit aucune colonne chiffrée.

Licence

MIT pour ce dépôt. garmin-mcp/ est une copie du projet amont de Philipp Born, sous licence MIT également : sa licence et ses notices d'origine voyagent avec le code, dans garmin-mcp/LICENSE et garmin-mcp/THIRD_PARTY_NOTICES.md. Ce dépôt n'est pas un produit officiel du projet amont.

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    B
    maintenance
    Connects MCP clients to Garmin Connect data, enabling queries about activities, sleep, heart rate, body battery, and training status.
    MIT
  • A
    license
    Not graded
    quality
    B
    maintenance
    MCP server for Garmin Connect that enables users to access and manage their personal health and fitness data, including daily summaries, heart rate, sleep, HRV, stress, body composition, activities, and training readiness, with secure per-user authentication.
    Apache 2.0
  • F
    license
    A
    quality
    C
    maintenance
    Provides read-only access to your Garmin Connect health data, including sleep, HRV, body battery, stress, training readiness, and activities, through an MCP server.
    14
    -
  • A
    license
    Not graded
    quality
    B
    maintenance
    Exposes Garmin Connect data through MCP, enabling retrieval of activities, health metrics, training status, and workouts, with OAuth authentication and confirmed write operations.
    MIT