Skip to main content
Glama

loom-plan

Serveur MCP qui donne à Claude Desktop une vue unifiée et actionnable de mon Google Agenda et de mes Google Tasks. Les retards d'abord, puis la journée, les créneaux libres et les tâches ouvertes.

Google reste l'unique référentiel. Le serveur ne stocke rien en dehors du token OAuth et d'un cache de lecture de trente secondes. Claude ne voit jamais un objet Google brut : événements et tâches arrivent dans un seul format, en heure locale, déjà triés dans l'ordre où je veux les lire.

Ce que ça fait

Quand je demande à Claude « qu'est-ce que j'ai aujourd'hui ? », il appelle le tool next et reçoit en un seul aller-retour tout ce qui est en retard, les événements de la journée, les trous entre eux et les tâches ouvertes, datées ou non. Il me présente ça en trois tableaux : En retard, Aujourd'hui, À faire.

Quand je dis « c'est fait », il marque l'événement ou la tâche. Quand je dis « ajoute une tâche pour vendredi », il la crée dans Google Tasks. Il ne fait jamais d'écriture de sa propre initiative : la règle figure dans la description de chaque tool d'écriture.

Quelques principes qui structurent tout le reste. Une chose passée et non traitée continue de remonter tant qu'elle n'est pas marquée faite ou annulée ; le passage du temps ne vaut pas traitement. Une ambiguïté est refusée avec un message clair plutôt que résolue par défaut : modifier une occurrence d'une série récurrente sans dire si on vise l'occurrence ou la série, c'est une erreur, pas une supposition. Une tâche ne bloque pas de temps, elle se fait dans les heures libres. Et le serveur fournit des données déjà ordonnées, la mise en forme finale reste à Claude.

Related MCP server: Schedule MCP Server

Installation

Il faut Python 3.12 ou plus, uv, et un compte Google.

git clone <ce dépôt> ~/dev/loom-plan
cd ~/dev/loom-plan
uv sync

Côté Google Cloud

Une fois, dans la console Google Cloud. Créer un projet, activer les API Google Calendar et Google Tasks. Dans Google Auth Platform, configurer l'écran de consentement en type Externe, ajouter les trois scopes ci-dessous dans Accès aux données, puis, dans Audience, publier l'application. Ce dernier point compte : en statut Test, Google fait expirer le refresh token au bout de sept jours et il faut se réauthentifier chaque semaine. En production sans validation, l'écran « application non vérifiée » s'affiche une fois au consentement et le token devient permanent.

https://www.googleapis.com/auth/calendar.events.owned
https://www.googleapis.com/auth/calendar.calendarlist.readonly
https://www.googleapis.com/auth/tasks

calendar.events.owned ne donne accès qu'aux agendas dont je suis propriétaire. Les abonnements en lecture seule (jours fériés, numéros de semaine) sont ignorés, et le token ne pourrait pas y écrire de toute façon.

Créer ensuite un client OAuth de type Application de bureau, télécharger son JSON et le placer à la racine du dépôt sous le nom client_secret.json. Ce fichier et token.json sont dans le .gitignore.

Configuration

Le serveur lit loom-plan.toml, dont le chemin est donné par la variable LOOM_PLAN_CONFIG ou par l'option --config, sinon dans le répertoire courant. Les chemins relatifs qu'il contient sont résolus par rapport au fichier lui-même.

[auth]
client_secret = "client_secret.json"
token = "token.json"

[general]
timezone = "Europe/Paris"
epoch = 2026-09-13
primary_calendar = "lamard.denis@gmail.com"
default_task_list = "Liste de lamard.denis"
notes_max_chars = 500
cache_ttl_seconds = 30

[work]
start = 09:00:00
end = 18:00:00
days = ["mon", "tue", "wed", "thu", "fri"]

[status]
done_prefix = "[fait]"
cancelled_prefix = "[annulé]"
tracked_prefix = "[suivi]"

L'epoch est la date de mise en service. Rien d'antérieur n'est considéré comme en retard, sinon tout l'historique de l'agenda remonterait au premier appel. Elle ne filtre que les retards : demander l'agenda de mars reste possible.

Premier lancement

uv run loom-plan auth     # ouvre le navigateur pour le consentement Google
uv run loom-plan check    # liste les agendas et les listes Tasks : les trois scopes sont validés d'un coup
uv run loom-plan next

Dans Claude Desktop

Dans claude_desktop_config.json, au même niveau que les autres serveurs :

"loom-plan": {
  "command": "/home/denis/dev/loom-plan/.venv/bin/loom-plan-mcp",
  "args": ["--config", "/home/denis/dev/loom-plan/loom-plan.toml"],
  "env": {}
}

Claude Desktop lance deux instances du serveur (chat et Cowork). Elles partagent token.json ; le rafraîchissement du token est protégé par un verrou fichier, et une instance relit le fichier avant de rafraîchir au cas où l'autre l'aurait déjà fait. Une lecture qui échoue sur un token expiré est retentée une fois.

Les tools

Lecture libre.

Tool

Rôle

next(hours?)

L'état de la journée en un appel : retards, événements, créneaux libres, tâches. Fenêtre par défaut jusqu'à la fin des horaires de travail.

overdue(include_recurring?)

Tout ce qui est passé et toujours ouvert, du plus ancien au plus récent.

agenda(start, end?, containers?, status?)

Événements et tâches datées sur une fenêtre de 31 jours maximum.

tasks(list_name?, status?)

Les tâches, y compris sans date.

find(query, start?, end?)

Recherche texte sur titres et notes, 90 jours en arrière et en avant par défaut.

free_slots(start, end, min_minutes?, work_start?, work_end?)

Créneaux libres calculés à partir des événements seuls, sur les jours ouvrés.

containers()

Agendas et listes disponibles.

Écriture, uniquement sur demande explicite.

Tool

Rôle

add_event(title, start, end, container?, notes?, location?, all_day?, recurrence?, tracked?)

Crée un événement, éventuellement récurrent (règle RRULE).

add_task(title, list_name?, due?, notes?)

Crée une tâche. Une liste inexistante est refusée, jamais créée.

update_event(id, scope?, ...)

Modifie un événement. scope obligatoire sur un récurrent : this ou series.

update_task(id, ...)

Modifie une tâche, y compris pour la déplacer de liste.

set_status(id, status)

open, done ou cancelled. Idempotent. Sur un récurrent, n'agit que sur l'occurrence.

delete(id, scope?)

Suppression définitive, réservée aux erreurs de saisie. Pour « ne plus faire », set_status(cancelled) garde la trace.

Chaque item porte un id qui encode son conteneur, evt_<agenda>/<id> ou task_<liste>/<id>, pour que les écritures sachent où frapper sans état côté serveur.

Les conventions de titre

Google Tasks distingue « à faire » de « terminé » mais pas « terminé » d'« abandonné », et Google Calendar n'a aucune notion de « fait ». Trois préfixes en tête de titre comblent ça. Ils sont retirés à la lecture et traduits en statut ; le serveur les réécrit lui-même à l'écriture.

Préfixe

Où

Sens

[fait]

événement

L'événement a été traité. Posé par set_status(done), ou à la main depuis n'importe quelle interface Google.

[annulé]

tâche

La tâche est terminée côté Google mais c'était un abandon. Posé par set_status(cancelled).

[suivi]

série récurrente

Chaque occurrence est une chose à faire, pas un rendez-vous qui se produit tout seul. Une occurrence passée sans [fait] remonte en retard. C'est le seul préfixe que je pose moi-même, à la création de la série.

Sans [suivi], les occurrences récurrentes sont exclues des retards : un point hebdo passé a eu lieu, personne ne le marque fait. Avec, une occurrence oubliée reste visible jusqu'à ce que je la traite, et la marquer faite ne touche que cette occurrence, jamais la série.

Une dernière convention, dans les notes cette fois : ~30min ou ~1h30 donne une durée estimée, utilisée pour trier les tâches du bloc À faire.

Ligne de commande

Les mêmes opérations sont disponibles en CLI, ce qui a servi à valider le mapping sur le vrai compte avant d'écrire le serveur.

uv run loom-plan next
uv run loom-plan overdue
uv run loom-plan agenda 2026-09-14 2026-09-21
uv run loom-plan tasks --status done
uv run loom-plan find notaire
uv run loom-plan free "2026-09-15 09:00" "2026-09-15 18:00" --min 45
uv run loom-plan add-task "Relancer le devis Dupont" --due 2026-09-19 --notes "~15min"
uv run loom-plan add-event "Regarder mon compte Malt" "2026-09-16 09:00" "2026-09-16 09:30" --recurrence "FREQ=WEEKLY;BYDAY=WE" --tracked
uv run loom-plan set-status task_<liste>/<id> done
uv run loom-plan serve     # le serveur MCP en stdio, équivalent de loom-plan-mcp

Quelques détails du mapping

Ce sont les points où Google ne dit pas ce qu'on croit, et qui ont chacun coûté une itération.

Un événement sur la journée entière a chez Google une date de fin exclusive : « du 14 au 17 » signifie du 14 au 16. Le serveur expose le vrai dernier jour et refait la conversion à l'écriture.

L'échéance d'une tâche est renvoyée sous la forme 2026-09-14T00:00:00.000Z, mais l'heure n'a aucun sens. Elle est lue comme une date pure et jamais convertie en heure locale, sinon on décale d'un jour.

Les descriptions d'événements sont en HTML. Les balises sont retirées, les sauts de ligne conservés, les entités décodées, et le tout est tronqué à 500 caractères avec un indicateur.

Un événement annulé n'est plus renvoyé par Google sans showDeleted, et pour un événement simple l'annulation équivaut à une mise à la corbeille. Demander status="cancelled" active ce paramètre.

Sur une récurrence, Google renvoie des occurrences dont l'id est celui de la série suffixé par la date. Marquer une occurrence faite crée une exception de la série, ce qui est exactement le comportement voulu.

Développement

uv run pytest -q
uv run ruff check src tests
uv run pyright

Les tests couvrent le mapping sur des payloads Google figés, le service avec les API Google simulées au niveau HTTP (respx), et le contrat des tools exposés par le serveur. Pyright est en mode strict ; les bibliothèques Google n'étant pas typées, une façade Protocol isole la frontière dans auth.py.

La spec complète, avec les décisions prises et leur justification, est dans spec-loom-plan.md.

Ce qui n'est pas là

Pas d'interface HTML interactive. Le crochet MCP Apps est en place (le tool next déclare une ressource ui://loom-plan/next.html), mais Claude Desktop ne rend pas encore les interfaces des serveurs personnalisés, ce qui est un bug connu de son côté. Le jour où ça change, la page minimale s'affichera d'elle-même et il sera temps d'écrire la vraie vue.

Pas de déclencheur d'actions automatiques, pas de synchronisation locale, pas d'agendas partagés tiers. Un seul agenda pour l'instant, mais le format des ids est prêt pour en ajouter un second sans migration.

Licence

Apache 2.0.

Available Tools

13 tools
add_eventA

ÉCRITURE : uniquement sur demande explicite de Denis. Ne jamais appeler de sa propre initiative, ni pour « aider », ni pour corriger. Renvoie l'objet résultant au format unifié. Crée un événement. start/end : format « AAAA-MM-JJ HH:MM » en heure locale Europe/Paris ; avec all_day=true, « AAAA-MM-JJ » du premier et du dernier jour (inclus). container : nom de l'agenda (défaut : agenda principal). recurrence : règle RRULE sans le préfixe, start/end donnant la première occurrence (ex. FREQ=WEEKLY;BYDAY=WE, FREQ=MONTHLY;BYMONTHDAY=1, FREQ=WEEKLY;BYDAY=MO,FR;COUNT=8). tracked=true (récurrence seulement) pose « [suivi] » : chaque occurrence est une chose à faire et remonte en retard si elle n'est pas marquée faite.

Inputs: end (required), start (required), title (required), notes, +5 more. Returns: result

add_taskA

ÉCRITURE : uniquement sur demande explicite de Denis. Ne jamais appeler de sa propre initiative, ni pour « aider », ni pour corriger. Renvoie l'objet résultant au format unifié. Crée une tâche. due : « AAAA-MM-JJ », optionnel. list_name : nom de la liste (défaut : liste configurée) ; une liste inexistante est refusée, jamais créée. Convention : « ~30min » ou « ~1h30 » dans notes donne une durée estimée.

Inputs: title (required), due, notes, list_name. Returns: result

agendaA
Read-only

Événements et tâches datées fusionnés et triés sur une fenêtre (31 jours maximum). start/end : format « AAAA-MM-JJ HH:MM » en heure locale Europe/Paris, ou « AAAA-MM-JJ » (minuit). end absent : start + 1 jour. containers : noms d'agendas ou de listes pour restreindre (voir containers). status : open (défaut), done ou cancelled. Présenter en tableau markdown.

Inputs: start (required), end, status, containers. Returns: result

containersA
Read-only

Agendas Google Calendar et listes Google Tasks disponibles (nom, id, principal, lecture seule). Sert à ranger correctement à l'écriture (container / list_name).

Inputs: no named parameters. Returns: result

deleteA
Destructive

ÉCRITURE : uniquement sur demande explicite de Denis. Ne jamais appeler de sa propre initiative, ni pour « aider », ni pour corriger. Renvoie l'objet résultant au format unifié. Suppression définitive, réservée aux erreurs de saisie. Pour « ne plus faire », utiliser set_status(cancelled). scope obligatoire pour un événement récurrent (this / series).

Inputs: id (required), scope. Returns: result

findA
Read-only

Recherche texte (titre, notes) sur événements et tâches, tous statuts. Sans fenêtre : 90 jours en arrière et 90 en avant. start/end : format « AAAA-MM-JJ HH:MM » en heure locale Europe/Paris.

Inputs: query (required), end, start. Returns: result

free_slotsA
Read-only

Créneaux libres entre start et end, calculés à partir des événements uniquement (les tâches ne bloquent pas de temps), sur les jours ouvrés, dans les horaires de travail (9h-18h par défaut ; work_start/work_end en « HH:MM »). format « AAAA-MM-JJ HH:MM » en heure locale Europe/Paris. min_minutes : durée minimale d'un créneau.

Inputs: end (required), start (required), work_end, work_start, +1 more. Returns: result

nextA
Read-only

État global de la journée en un seul appel : (1) En retard — tout ce qui est passé et toujours ouvert ; (2) événements de la fenêtre ; (3) créneaux libres ; (4) tâches ouvertes, y compris sans date, triées par échéance puis durée estimée. À appeler pour toute question du type « qu'est-ce que j'ai aujourd'hui / à faire ». Présenter le résultat en markdown, en trois blocs — En retard, Aujourd'hui, À faire — chacun sous forme de tableau (heure ou échéance, titre, durée estimée, agenda/liste). Les créneaux libres s'intercalent dans le tableau Aujourd'hui. Ne pas omettre le bloc En retard s'il est vide : l'indiquer explicitement. Ne pas afficher les ids sauf si Denis les demande. hours : fenêtre en heures à partir de maintenant ; par défaut jusqu'à la fin des horaires de travail du jour (ou minuit si on est déjà après).

Inputs: hours. Returns: result

overdueA
Read-only

Tout ce qui est passé et toujours ouvert (tâches à échéance dépassée, événements terminés non marqués faits), du plus ancien au plus récent. Sert seul pour « qu'est-ce que je n'ai pas fait » ; sinon son contenu est déjà dans next. Les occurrences d'événements récurrents sont exclues sauf include_recurring=true — à l'exception des séries dont le titre commence par « [suivi] » (convention posée par Denis dans Google Agenda : chaque occurrence est une chose à faire), toujours incluses.

Inputs: include_recurring. Returns: result

set_statusA

ÉCRITURE : uniquement sur demande explicite de Denis. Ne jamais appeler de sa propre initiative, ni pour « aider », ni pour corriger. Renvoie l'objet résultant au format unifié. Change le statut d'un événement ou d'une tâche : open, done, cancelled. Idempotent. Pour un événement récurrent, agit sur l'occurrence seulement. C'est le tool à utiliser pour « c'est fait » / « je ne le ferai pas » (préférer cancelled à delete : on garde la trace).

Inputs: id (required), status (required). Returns: result

tasksA
Read-only

Tâches Google Tasks, datées ou non — le seul tool qui remonte les tâches sans date en dehors de next. list_name : nom d'une liste (défaut : toutes). status : open (défaut), done ou cancelled.

Inputs: status, list_name. Returns: result

update_eventA

ÉCRITURE : uniquement sur demande explicite de Denis. Ne jamais appeler de sa propre initiative, ni pour « aider », ni pour corriger. Renvoie l'objet résultant au format unifié. Modifie un événement (titre, horaire, notes, lieu). scope obligatoire pour un événement récurrent : « this » (cette occurrence) ou « series » (toute la série) — absent sur un récurrent, le serveur refuse. start et end vont ensemble ; format « AAAA-MM-JJ HH:MM » en heure locale Europe/Paris.

Inputs: id (required), end, notes, scope, +4 more. Returns: result

update_taskA

ÉCRITURE : uniquement sur demande explicite de Denis. Ne jamais appeler de sa propre initiative, ni pour « aider », ni pour corriger. Renvoie l'objet résultant au format unifié. Modifie une tâche : titre, notes, échéance (due « AAAA-MM-JJ », ou clear_due=true pour la retirer), liste (list_name : déplacement).

Inputs: id (required), due, notes, title, +2 more. Returns: result

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    A4.1/5.0

    Scored across 13 tools

    Disambiguation4/5

    The read tools overlap in scope: `next` aggregates `overdue`, `agenda`, `tasks`, and `free_slots`, while `agenda`, `tasks`, and `find` cover intersecting data. The descriptions explicitly clarify intended use cases (daily overview vs. unfinished items vs. arbitrary window vs. search), so an agent can usually choose correctly, but the boundaries require careful reading.

    Naming Consistency3/5

    Read tools use bare nouns or adjectives (`next`, `agenda`, `tasks`, `overdue`, `containers`, `free_slots`) while write tools use verb_noun or verb patterns (`add_event`, `update_task`, `set_status`, `delete`). This is readable and arguably intentional as a read/write split, but it is not a single predictable naming convention.

    Tool Count5/5

    13 tools is well within the ideal 3–15 range for a personal calendar and task assistant. Each tool covers a distinct operation—daily overview, search, availability, CRUD for events and tasks—so none feels superfluous.

    Completeness5/5

    The surface provides full lifecycle coverage: create, read, update, delete, and status changes for both events and tasks, plus search, free-slot calculation, and container discovery. No obvious domain operation is missing for the stated purpose.

    Maintenance

    ActivitySlowing
    ResponsivenessNo recent community issues

    Related MCP Connectors

    Related MCP Servers