Notez offre deux façons de brancher une IA sur vos notes. Il y a le chat dans l’application, et il y a le serveur MCP, par lequel votre propre assistant — Claude Code, Cursor, celui que vous utilisez — lit et écrit dans le même espace de travail.
Ils devaient être deux portes pour une même pièce. La semaine dernière, quelqu’un a posé une question raisonnable : l’assistant peut-il supprimer une note ? La réponse était non des deux côtés, et c’était très bien. La réponse à la question suivante était plus gênante.
Seize contre dix
Le chat avait seize outils. MCP en avait dix.
Cet écart ne venait d’aucune décision. En août, j’avais appris au chat de l’application à lire et écrire les propriétés personnalisées, les étiquettes et les colonnes de tableau : six nouveaux outils. Ce travail est parti dans le chat et s’est arrêté là. Personne n’avait décidé que MCP ne les aurait pas. Personne n’avait remarqué qu’il ne les avait pas.
Pire, la partie commune était incohérente, au point que les deux ressemblaient à des produits différents. Le chat avait search_notes ; MCP avait list_notes, qui faisait la même chose avec plus de filtres. Le chat avait semantic_search ; MCP l’appelait search_similar_notes. Même fonction, nom différent, forme différente : le genre de divergence que personne ne planifie et sur laquelle tout le monde finit par trébucher.
C’est ainsi qu’un logiciel pourrit, d’ordinaire. Pas par une mauvaise décision, mais par une décision qu’on n’a pas répétée : un changement juste pour la partie qu’il touchait, et muet sur celle qu’il ne touchait pas.
Ce que je n’ai pas unifié
La solution évidente est une implémentation partagée qu’appellent les deux côtés. Je l’ai essayée, puis j’ai fait marche arrière, parce que deux différences se sont révélées indispensables.
Les résultats doivent différer. MCP répond en JSON et lève des erreurs typées, parce qu’il parle à un programme. Le chat répond en prose avec des liens comme [Budget 2024](note:a1b2…), parce que l’application les analyse pour afficher les petites pastilles cliquables de notes sous une réponse. Faites renvoyer du JSON au chat, et les pastilles disparaissent.
Les erreurs aussi doivent différer. Quand le chat rencontre un problème, il renvoie une phrase (« oldText est introuvable dans la note ; relis-la au lieu de deviner »), parce que le modèle lit cette phrase et se corrige au tour suivant, dans un budget de cinq tours. MCP lève une exception et laisse le client décider. Imposez un seul contrat aux deux, et l’un des deux se dégrade.
J’ai donc unifié la partie qui doit être identique (quels outils existent, comment ils s’appellent, quels arguments ils prennent) et laissé la présentation propre à chaque côté. Le garde-fou est un test qui construit la vraie liste d’outils MCP, la compare aux déclarations du chat et fait échouer le build à la moindre différence. Il y avait exactement une exception autorisée, écrite dans le test avec sa raison : déplacer une carte entre les colonnes d’un tableau restait réservé à l’application, parce qu’un tableau semblait être une disposition visuelle qu’un assistant, de l’autre côté d’une API, ne pouvait pas voir. Cette exception a disparu : aujourd’hui MCP déplace aussi les cartes entre colonnes, et les seuls outils de MCP que le chat n’a pas sont les deux qui importent des fichiers et, avec une connexion par compte, celui qui liste vos espaces de travail.
Un commentaire disant « garder ces listes synchronisées » n’aurait rien coûté. Il aurait aussi été ignoré, comme le précédent.
Puis j’ai voulu ajouter « déplacer »
Supprimer, ajouter aux favoris et créer directement à l’intérieur d’une autre note ont été simples. Déplacer une note sous une autre semblait tout aussi simple : l’application a un élément de menu Déplacer sous… depuis des mois, donc le plus dur était forcément fait.
Il ne l’était pas.
Notez stocke l’arbre des notes dans une table de fermeture (closure table) : pas seulement « le parent de B est A », mais une ligne pour chaque paire ancêtre-descendant, avec la distance entre les deux. Ainsi, « montre-moi toutes les notes sous celle-ci, à n’importe quelle profondeur » devient une seule requête indexée au lieu d’un parcours récursif.
Le code de déplacement réécrivait ces lignes pour la note déplacée. Uniquement pour la note déplacée.
Avec A > B > C, déplacer B sous D donnait donc ceci :
Ancêtres de
B:D. Correct.Ancêtres de
C:AetB. Faux.An’a plus rien à voir avecC, etDmanque complètement.
Le fil d’Ariane de C affichait Tree A › Tree B › Tree C alors que B se trouvait en réalité sous D. Le bug était accessible depuis le menu de l’application depuis le début. Il suffisait que quelqu’un déplace une note qui avait déjà des enfants, puis regarde l’un de ces enfants.
J’étais sur le point de livrer un outil avec lequel un assistant aurait pu le faire cent fois par minute.
Le test que j’ai failli ne pas écrire
La correction lit le sous-arbre, supprime uniquement les lignes qui en sortent et rattache chaque descendant aux nouveaux ancêtres. Les tests unitaires passaient. Ils simulent la base de données, donc ce qu’ils confirment vraiment, c’est que le code fait ce que son auteur attendait.
Le test de bout en bout (un vrai navigateur, une vraie base de données, déplacer une note qui a un enfant, descendre jusqu’au petit-enfant et lire son fil d’Ariane) a échoué au premier lancement. Avec la correction en place. C’est précisément pour ça que je l’avais écrit.
La cause était un détail que personne n’aurait deviné et qu’aucun mock n’aurait révélé : une note créée au premier niveau n’a pas de ligne vers elle-même dans la table de fermeture. Toutes les autres notes ont une ligne qui dit « je suis mon propre ancêtre, à distance zéro » ; une simple note de premier niveau n’en a pas, parce que seul le chemin de code qui rattache un parent l’écrit. Donc « sélectionner tout ce qui est sous cette note » renvoyait les enfants mais pas la note elle-même, et l’étape de suppression, chargée d’effacer les liens qui sortent du sous-arbre, a allègrement coupé le lien entre la note et ses propres enfants.
Résultat : le nouveau parent se retrouvait sans aucun enfant, et le seul ancêtre du petit-enfant était une note deux niveaux au-dessus.
Une vérification manuelle antérieure était passée. Par chance : la note que j’avais prise pour tester avait été déplacée par l’ancien code, qui écrivait toujours une ligne vers elle-même, donc elle en avait une.
Voilà tout l’argument en faveur des tests qui touchent le vrai système. Un mock encode ce que vous croyez. Une base de données encode ce qui est vrai. Quand les deux divergent, mieux vaut l’apprendre par un test que par quelqu’un dont les notes se sont discrètement réorganisées.
Ce que vous y gagnez
Les deux côtés ont maintenant les mêmes dix-huit outils. Votre assistant range une note sous une autre, l’ajoute aux favoris, la met à la corbeille, et vous dit qu’elle a emporté quatre sous-notes avec elle : vous connaissez l’ampleur de ce qui vient de se passer avant de décider s’il faut l’annuler.
Deux corrections plus petites ont suivi. Lister les notes via MCP renvoyait les notes de la corbeille mêlées aux autres, sauf exclusion explicite, ce qui contredisait sa propre documentation. Et demander au chat « sur quoi ai-je travaillé ? » triait par date de création, ce qui répondait à une question sur cette semaine avec des notes de mars.
Rien ici ne fera l’objet d’une capture d’écran. C’est la version du produit qui fait ce que ses propres pages d’aide disaient déjà qu’il faisait, et à un certain stade de la vie d’un projet, c’est le genre de version le plus utile.
À lire aussi
Pourquoi j’ai créé Notez — les deux problèmes de départ de Notez, la capture et la recherche, et ce que son chat IA peut ou ne peut pas changer dans vos notes.
Quatre filets de sécurité pour vos notes, de l’annulation à l’enregistrement refusé — la pile d’annulation de 20 actions, 30 jours d’historique des versions, la corbeille de 30 jours et le garde-fou qui refuse un enregistrement périmé.
Notez, une application de prise de notes pour les développeurs — ce qu’est Notez, d’où arrivent les notes, comment la recherche les retrouve et ce que coûte chaque plan.