@imenam/mcp-messenger 1.0.32 → 1.0.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,772 +1,832 @@
1
- # MCP Messenger
2
-
3
- Boîte aux lettres pour agents IA : un **serveur** central reçoit et conserve les messages,
4
- et un **client MCP** donne à chaque agent — quelle que soit sa plateforme — les outils pour
5
- écrire, lire, classer et attendre des réponses.
6
-
7
- Le même paquet joue les deux rôles :
8
-
9
- | | Mode serveur (`--server`) | Mode MCP (sans argument) |
10
- |---|---|---|
11
- | Rôle | Boîte aux lettres : stockage, distribution, annuaire | Client stdio branché sur un agent |
12
- | Exposé sur | HTTP/HTTPS (API + console web) | stdin/stdout (JSON-RPC MCP) |
13
- | Consommé par | Les MCP des agents, la console web | Claude Code, Cursor, tout client MCP |
14
- | Variables clés | `MESSENGER_PORT`, `MESSENGER_TOKEN` | `MESSENGER_AGENT_ID`, `MESSENGER_SERVER_URL`, `MESSENGER_TOKEN` |
15
- | Processus | 1 pour toute la flotte | 1 par agent |
16
-
17
- ```mermaid
18
- flowchart LR
19
- A["Agent A (Claude Code)"] -- "stdio" --> MA["mcp-messenger (MCP)"]
20
- B["Agent B (Cursor)"] -- "stdio" --> MB["mcp-messenger (MCP)"]
21
- C["Agent C (autre plateforme)"] -- "stdio" --> MC["mcp-messenger (MCP)"]
22
- MA -- "HTTP(S)" --> S
23
- MB -- "HTTP(S)" --> S
24
- MC -- "HTTP(S)" --> S
25
- S["mcp-messenger --server\nSQLite + API + console"] --> U["Console web /ui"]
26
- ```
27
-
28
- Les agents n'ont pas besoin d'être sur la même machine ni sur la même plateforme : ils
29
- partagent seulement l'adresse du serveur. **Chaque session d'agent reçoit sa propre boîte**,
30
- identifiée par une adresse opaque du type `box_9f2c4a01b3d7e5f8` — voir
31
- [Adresses et sessions](#adresses-et-sessions).
32
-
33
- ---
34
-
35
- ## Démarrage rapide
36
-
37
- ### 1. Lancer la boîte aux lettres
38
-
39
- ```bash
40
- # Deux jetons distincts : celui des agents, et celui de la console.
41
- npx @imenam/mcp-messenger --generate-token # → jeton client
42
- npx @imenam/mcp-messenger --generate-token # → jeton console
43
-
44
- MESSENGER_TOKEN=<jeton-client> MESSENGER_ADMIN_TOKEN=<jeton-console> \
45
- npx @imenam/mcp-messenger --server
46
- # → listening on http://127.0.0.1:3600 (loopback only)
47
- # → auth: shared token — required on every request
48
- # → WARN shared token: no isolation between mailboxes — any holder can act as any address
49
- # → WARN set MESSENGER_AGENT_TOKENS (one token per agent) to compartmentalize
50
- # → web console: http://127.0.0.1:3600/ui/ (asks for MESSENGER_ADMIN_TOKEN once)
51
- ```
52
-
53
- Trois invariants, non négociables et vérifiés par les tests :
54
-
55
- 1. **Le serveur écoute toujours sur `127.0.0.1`.** `MESSENGER_HOST` n'est pas un réglage :
56
- toute autre valeur fait échouer le démarrage. Pour joindre la boîte depuis une autre
57
- machine, il faut placer un reverse proxy (TLS) devant.
58
- 2. **Un jeton est obligatoire et vérifié sur chaque requête de données** — API, `/health`
59
- et données de la console (`/admin`) comprises. Seule la page de la console, qui ne
60
- contient aucun secret, est servie librement. Sans `MESSENGER_TOKEN` ni
61
- `MESSENGER_AGENT_TOKENS`, le serveur refuse de démarrer.
62
- 3. **Le jeton de la console n'est jamais celui des agents.** `MESSENGER_ADMIN_TOKEN` est
63
- exigé dans tous les cas, et le démarrage échoue s'il vaut `MESSENGER_TOKEN`. La console
64
- voit toutes les boîtes de tous les projets et sait les supprimer, alors que le jeton
65
- client est recopié dans le `.mcp.json` de chaque projet : les confondre donnerait ce
66
- pouvoir à tout porteur de ce fichier.
67
-
68
- > **Le mode jeton partagé ne cloisonne pas les boîtes.** `MESSENGER_TOKEN` atteste
69
- > l'appartenance à la flotte, pas une adresse : l'en-tête `X-Agent-Id` qui accompagne chaque
70
- > appel n'est alors adossé à rien, et tout porteur du jeton peut lire, envoyer et supprimer
71
- > au nom de n'importe quelle boîte. C'est un mode « cercle de confiance », adapté aux
72
- > sessions d'une même personne sur une même machine — le serveur le rappelle au démarrage.
73
- > Dès que les agents ne se font pas mutuellement confiance, passer à
74
- > [`MESSENGER_AGENT_TOKENS`](#serveur), qui lie chaque adresse à son propre
75
- > jeton et rejette toute autre combinaison.
76
-
77
- ### 2. Déclarer le MCP côté agent
78
-
79
- Dans le dossier du projet de l'agent :
80
-
81
- ```bash
82
- npx -y @imenam/mcp-messenger --claude-setup-mcp
83
- ```
84
-
85
- La commande écrit l'entrée `mcp-messenger` dans le `.mcp.json` du répertoire courant (les
86
- autres serveurs sont préservés ; `--force` pour remplacer une entrée existante), avec un
87
- `MESSENGER_AGENT_ID` pré-rempli d'après le nom du dossier. Il ne reste qu'à coller le jeton
88
- du serveur dans `MESSENGER_TOKEN`.
89
-
90
- Le bloc généré, à écrire à la main si tu préfères :
91
-
92
- ```json
93
- {
94
- "mcpServers": {
95
- "messenger": {
96
- "command": "npx",
97
- "args": ["@imenam/mcp-messenger"],
98
- "env": {
99
- "MESSENGER_AGENT_ID": "planner",
100
- "MESSENGER_SERVER_URL": "http://127.0.0.1:3600",
101
- "MESSENGER_TOKEN": "le-jeton-du-serveur",
102
- "MESSENGER_AGENT_NAME": "Planner",
103
- "MESSENGER_AGENT_DESCRIPTION": "Découpe les tâches et répartit le travail"
104
- }
105
- }
106
- }
107
- }
108
- ```
109
-
110
- Chaque agent reçoit le même bloc avec **son propre** `MESSENGER_AGENT_ID`. Au démarrage, le
111
- MCP s'enregistre dans l'annuaire du serveur ; si le serveur est momentanément injoignable,
112
- la session MCP démarre quand même et l'enregistrement se fera au premier appel réussi.
113
-
114
- ### 3. Écrire à un autre agent
115
-
116
- L'agent A appelle `send_message` avec `to: ["coder"]`, l'agent B voit le message via
117
- `list_messages` puis `read_message`, et répond avec `reply_to` pour rester dans le fil.
118
- Le fil se nomme au passage (`thread_name` sur `send_message`, ou `rename_thread` plus tard).
119
- `wait_for_message` permet d'attendre une réponse sans faire tourner une boucle d'appels : il ne
120
- rend la main que sur un message arrivé *après* le début de l'attente, les non-lus déjà en boîte
121
- ne l'interrompent pas.
122
-
123
- ---
124
-
125
- ## Être notifié sans attendre
126
-
127
- Attendre coûte un tour d'agent. Pour rester joignable **sans rien bloquer**, l'agent active
128
- les notifications une fois en début de session ; ensuite il travaille normalement, et les
129
- messages entrants l'interrompent d'eux-mêmes — y compris quand il est à l'arrêt, inactif.
130
-
131
- ```
132
- enable_notifications() → retourne immédiatement, ne bloque rien
133
-
134
- Notifications enabled for mailbox `box_9f2c4a01b3d7e5f8`.
135
-
136
- Last step — call the Monitor tool now, with exactly these arguments:
137
- command: npx -y @imenam/mcp-messenger --watch box_9f2c4a01b3d7e5f8
138
- description: Incoming messenger messages
139
- timeout_ms: 1800000
140
- ```
141
-
142
- L'agent recopie ces trois arguments dans l'outil `Monitor` de son hôte. Le veilleur tourne en fond, tient une attente longue sur `/api/wait`, et écrit **une
143
- ligne par message** sur sa sortie standard :
144
-
145
- ```
146
- [messenger] planner — Refacto prêt (msg_4c1e88a0b2)
147
- ```
148
-
149
- Chaque ligne réveille l'agent, qui appelle `read_message` s'il décide de traiter le message.
150
- Une liaison posée depuis la console sur sa boîte produit elle aussi une ligne, qui nomme la
151
- boîte d'en face et renvoie vers `list_links` :
152
-
153
- ```
154
- [messenger] linked with Documentor Dev · Intégration RSS (box_3a7e19c0d4b2f6e1) — Brancher le flux RSS — call list_links
155
- ```
156
-
157
- **Réarmer à l'expiration.** Claude Code arrête tout `Monitor` au bout de 30 minutes au plus,
158
- quelle que soit la durée demandée, et prévient l'agent. Ce minuteur appartient à l'hôte : le
159
- veilleur ne peut ni le prolonger ni le remettre à zéro. L'agent relance donc le même appel
160
- `Monitor` à chaque expiration, sans repasser par `enable_notifications` — la réponse de cet
161
- outil, les instructions du serveur et le bloc `CLAUDE.md` le lui disent. Le délai de grâce
162
- décrit plus bas couvre l'intervalle : `whoami` reste à « armed » pendant le réarmement.
163
-
164
- **Pourquoi deux appels et pas un.** Armer un réveil, c'est ouvrir un canal vers la boucle de
165
- la session hôte. Un serveur MCP ne peut pas le faire seul : `Monitor` est un outil de l'hôte,
166
- seul le modèle peut l'appeler. `enable_notifications` fait donc tout le reste — il connaît
167
- l'adresse, il prépare le terrain — et rend la commande exacte à recopier. L'agent ne compose
168
- rien.
169
-
170
- **Où passe le jeton.** Le veilleur est lancé par l'hôte dans le shell de la session : il
171
- n'hérite pas du bloc `env` du `.mcp.json`, donc ni de `MESSENGER_SERVER_URL` ni de
172
- `MESSENGER_TOKEN`. Les mettre sur la ligne de commande les afficherait dans la conversation.
173
- `enable_notifications` dépose donc un relais en `0600` sous `~/.mcp-messenger/watch/`
174
- (`MESSENGER_STATE_DIR` pour le déplacer), que `--watch` retrouve à partir de la seule adresse.
175
- Le fichier est nommé par hachage de l'adresse, supprimé à la fin de la session, et purgé au
176
- bout de sept jours si la session s'est arrêtée brutalement.
177
-
178
- **Savoir si c'est vraiment armé.** L'armement dépend de l'agent : rien ne peut le déclencher
179
- à sa place, et un agent qui saute l'étape se croirait joignable sans l'être. Le serveur suit
180
- donc les attentes tenues par un veilleur, et `whoami` le dit :
181
-
182
- ```
183
- Notifications: armed — incoming messages will interrupt you (watcher since 2026-09-07T13:31:00.203Z).
184
- Notifications: NOT armed — no incoming message will reach you. Call enable_notifications.
185
- ```
186
-
187
- C'est une présence de connexion, pas une donnée stockée : elle vit en mémoire du serveur et
188
- disparaît avec lui. Un `wait_for_message` ordinaire ne compte pas — il s'arrête au premier
189
- message, un veilleur reste. Entre deux attentes longues, un délai de grâce de deux minutes
190
- évite que le signalement clignote. `enable_notifications` s'en sert aussi : si un veilleur est
191
- déjà en place, il le dit et n'en fait pas armer un second, qui doublerait chaque notification.
192
-
193
- **Ce que ça ne couvre pas.** Le veilleur n'annonce que les messages arrivés **après** son
194
- démarrage : `enable_notifications` signale le nombre de non-lus déjà en boîte, à traiter avec
195
- `list_messages`. Et `--claude-setup-mcp` écrit un rappel d'armement dans le `CLAUDE.md` du
196
- projet, pour que l'étape ne soit pas oubliée en début de session.
197
-
198
- **Sur un autre hôte que Claude Code.** `--watch` est une commande ordinaire : n'importe quel
199
- superviseur capable de lire les lignes d'un processus peut la brancher. Sans relais, elle lit
200
- `MESSENGER_SERVER_URL`, `MESSENGER_TOKEN` et `MESSENGER_AGENT_ID` dans son environnement.
201
-
202
- ---
203
-
204
- ## Outils MCP
205
-
206
- **Deux mots, deux choses.** Une **boîte** (`mailbox` dans les outils) est une adresse
207
- `box_…` : elle désigne une session, et c'est à elle qu'on écrit. Un **nom d'agent** est le
208
- projet pour lequel une session travaille : c'est une étiquette, jamais une adresse, et
209
- toutes les sessions du projet la portent. Partout où un outil attend une adresse, sa
210
- description dit « mailbox address ».
211
-
212
- | Outil | Rôle |
213
- |---|---|
214
- | `whoami` | Adresse de cette session, boîtes liées, serveur connecté, veilleur armé ou non, compteurs par dossier |
215
- | `attach_to_mailbox` | Se brancher sur une autre boîte via son adresse ; sans argument, retour à la sienne |
216
- | `rename_mailbox` | Nommer ou renommer sa boîte (de quoi la session s'occupe) ; le nom de l'agent, posé par la configuration, reste |
217
- | `rename_thread` | Nommer ou renommer un fil (`thread_id`) ; le nom est partagé par tous ses participants |
218
- | `list_threads` | Carte des conversations : nom, participants, non-lus, dernière activité ; `with_mailbox` ne garde que celles partagées avec une boîte |
219
- | `list_agents` | Annuaire groupé par agent : un projet, ses sessions ; `agent_name` déplie un groupe. La boîte de l'appelant n'y figure pas ; une boîte liée à la sienne porte « linked to you » |
220
- | `list_links` | Boîtes que l'utilisateur a liées à celle-ci depuis la console, la plus récente en tête, avec le motif donné |
221
- | `send_message` | Écrire à un agent — un seul destinataire par message (`to`, `priority`, `reply_to`, `thread_name`, `attachments`) |
222
- | `list_messages` | Lister la boîte (filtres : dossier, non lus, expéditeur, fil, texte libre) |
223
- | `read_message` | Lire un message en entier, pièces jointes listées ; le marque lu par défaut |
224
- | `get_thread` | Relire toute une conversation, du plus ancien au plus récent |
225
- | `save_attachment` | Écrire une pièce jointe sur le disque de la machine (`attachment_id`, `path`, `overwrite`) et rendre son chemin |
226
- | `mark_messages` | Marquer lu / non lu, suivi / non suivi |
227
- | `move_messages` | Classer dans un dossier |
228
- | `delete_messages` | Corbeille, ou suppression définitive (`permanent: true`) |
229
- | `list_folders` | Dossiers avec totaux et non-lus |
230
- | `create_folder` / `delete_folder` | Organiser l'espace comme une messagerie |
231
- | `enable_notifications` | Prépare la notification passive et rend la commande `Monitor` à armer ; ne bloque rien |
232
- | `wait_for_message` | Attente longue (jusqu'à 300 s) jusqu'à l'arrivée d'un message *postérieur* à l'appel |
233
-
234
- **Noms lisibles** — voir « Nommer les boîtes et les fils » ci-dessous.
235
-
236
- **Un message, deux boîtes** — un échange lie exactement deux agents *distincts* : `to` prend
237
- une seule adresse, il n'y a ni diffusion ni copie, et une boîte ne peut pas s'écrire à
238
- elle-même. Pour toucher plusieurs agents, on leur envoie un message chacun, dans son propre
239
- fil. C'est aussi pourquoi `list_agents` ne montre jamais l'appelant : la seule adresse qu'il
240
- ne peut pas viser n'a pas à figurer dans son annuaire.
241
-
242
- **Pièces jointes** — `send_message` accepte `attachments: [{ path, name? }]` : le processus
243
- MCP lit chaque fichier sur la machine de l'agent qui écrit et l'envoie avec le message.
244
- Au plus 10 fichiers, 10 Mo chacun et 25 Mo au total ; un fichier absent ou trop gros fait
245
- échouer l'envoi. Le type MIME est déduit de l'extension, `name` renomme le fichier pour le
246
- destinataire. Celui-ci voit la liste sous le message (`att_…`, nom, type, taille) et
247
- récupère un fichier avec `save_attachment` : dans le répertoire donné sous son propre nom,
248
- au chemin de fichier donné, ou par défaut dans le dossier temporaire du système. Un fichier
249
- existant n'est remplacé qu'avec `overwrite: true`. Les pièces jointes sont stockées dans la
250
- base avec le message et disparaissent avec sa dernière copie.
251
-
252
- **Dossiers système** — toujours présents, non supprimables : `inbox`, `sent`, `archive`,
253
- `trash`. Les dossiers personnalisés sont propres à chaque boîte.
254
-
255
- **Ce que voit un agent** : uniquement les livraisons de la boîte à laquelle il est attaché. Un message adressé à `coder`
256
- est invisible pour tout autre agent, y compris via son identifiant. Supprimer un message ne
257
- touche que sa propre copie : l'autre partie garde la sienne.
258
-
259
- ---
260
-
261
- ## Adresses et sessions
262
-
263
- Une adresse n'est pas un rôle, c'est une **boîte**. Au démarrage, chaque processus MCP —
264
- donc chaque session d'agent — s'en fait allouer une, avec un identifiant opaque et unique :
265
-
266
- ```
267
- box_9f2c4a01b3d7e5f8
268
- ```
269
-
270
- Conséquence directe : deux sessions ouvertes dans le même projet, avec le même `.mcp.json`,
271
- ont deux boîtes distinctes et ne se volent pas leur courrier.
272
-
273
- ### Faire communiquer deux agents
274
-
275
- Le plus simple est de **lier leurs deux boîtes depuis la console web** (onglet
276
- « Liaisons », voir « Console web ») : chaque agent reçoit la liaison sans qu'aucune adresse
277
- ne passe par toi. Un agent dont les notifications sont armées en est prévenu aussitôt ; les
278
- autres la trouvent dans `whoami` ou `list_links`.
279
-
280
- Sans liaison, l'agent B ouvre `list_agents` et y trouve l'adresse de A.
281
-
282
- Dans tous les cas, une adresse ne circule jamais **dans** un message : le serveur y attache
283
- déjà l'expéditeur et le destinataire. Un agent qui recopie une adresse dans un corps de
284
- message écrit une information que le lecteur a déjà, et qui sera fausse le jour où il se
285
- trompe de session.
286
-
287
- Comme les adresses sont opaques, demande aux agents de nommer leur boîte
288
- (`rename_mailbox`) : le nom apparaît dans `list_agents` et dans la console web, ce
289
- qui permet de retrouver une adresse perdue.
290
-
291
- ### Deux noms par boîte : l'agent et la boîte
292
-
293
- Une boîte porte deux identités, qui répondent à deux questions différentes et ne changent
294
- pas au même rythme :
295
-
296
- | | D'où vient-il ? | De quoi s'occupe-t-il ? |
297
- |---|---|---|
298
- | Champ | `agent_name`, `agent_description` | `display_name`, `description` |
299
- | Posé par | la configuration (`MESSENGER_AGENT_NAME`, `MESSENGER_AGENT_DESCRIPTION`) | l'agent lui-même, via `rename_mailbox` |
300
- | Change | jamais depuis une session | librement, au fil du travail |
301
- | Exemple | `Ai Corrector Dev` | `Correction du parseur de copies` |
302
-
303
- Le nom de l'agent seul ne distingue pas deux sessions ouvertes sur le même projet ; le nom
304
- de la boîte seul ne dit pas d'où l'agent écrit. `whoami` et la console web montrent donc
305
- toujours les deux, et l'annuaire (`list_agents`) les met l'un sous l'autre : le nom de
306
- l'agent en tête de groupe, une ligne par boîte en dessous. Renommer la boîte ne touche
307
- jamais au nom de l'agent.
308
-
309
- **Un nom d'agent ne désigne personne.** C'est le piège de ce modèle : plusieurs boîtes
310
- portent le même `agent_name`, une par session lancée depuis la même configuration.
311
- L'annuaire le rend visible en groupant les boîtes sous leur nom d'agent — un groupe à
312
- plusieurs lignes, ce sont plusieurs sessions vivantes du même projet, chacune à sa propre
313
- adresse. Un groupe nombreux n'affiche que ses trois sessions les plus récentes ;
314
- `list_agents` avec `agent_name` donne le reste, sans imposer ce second appel dans le cas
315
- courant. Un agent
316
- qui croise son propre nom d'agent dans l'annuaire regarde une *autre* session du même
317
- projet, jamais la sienne — sa boîte, elle, n'y figure pas du tout. Seule l'adresse identifie
318
- une session. Ce rappel est servi à trois endroits, du plus tôt au plus tard : les
319
- instructions du serveur MCP (reçues à la connexion, avant le premier outil), la ligne qui
320
- suit `Agent:` dans `whoami`, et le pied de l'annuaire dans `list_agents`.
321
-
322
- **Les adresses ne se recopient pas à la main.** Le serveur attache l'expéditeur et le
323
- destinataire à chaque message, et le lecteur les voit dans l'en-tête. Écrire son adresse
324
- dans le corps n'ajoute rien et se met à mentir dès qu'elle est erronée ou périmée —
325
- `send_message` le dit dans sa description et dans celle de `body`.
326
-
327
- ### Nommer les boîtes et les fils
328
-
329
- Les identifiants (`box_…`, `thr_…`) ne disent rien de ce qui se passe. Deux noms en langue
330
- naturelle viennent par-dessus, un outil pour chacun :
331
-
332
- ```
333
- rename_mailbox(name: "Développement des tests unitaires du parser")
334
- → nomme la boîte de la session (le nom de l'agent reste)
335
-
336
- rename_thread(thread_id: "thr_…", name: "Le test échoue sur un bug applicatif")
337
- → nomme le fil
338
- ```
339
-
340
- Le plus simple reste de nommer un fil au moment où on l'ouvre, via `thread_name` sur
341
- `send_message` : l'agent qui écrit le premier message sait déjà de quoi il parle.
342
-
343
- L'ancien outil `set_label`, qui cumulait les deux rôles, reste accepté sans être publié :
344
- une session lancée avant la mise à jour peut encore l'appeler.
345
-
346
- Un fil n'a **qu'un seul nom, partagé par tous ses participants** — ils discutent du même
347
- sujet, ils doivent lire le même intitulé. N'importe quel participant peut le renommer, et
348
- le dernier a le dernier mot ; le serveur retient qui (`named_by`). Un agent extérieur au
349
- fil ne peut pas le renommer.
350
-
351
- Tant qu'un fil n'a pas été nommé, l'affichage retombe sur le sujet de son message
352
- d'ouverture : rien n'est jamais illisible, mais rien n'est aussi parlant qu'un nom choisi.
353
- Ces noms apparaissent dans `list_threads`, `list_messages`, `get_thread`, les
354
- notifications du veilleur et la console web.
355
-
356
- ### Reprendre le travail d'une session interrompue
357
-
358
- Une session relancée est un nouveau processus. Sans mémoire, elle se ferait allouer une
359
- boîte neuve et laisserait derrière elle ses fils, ses non-lus et **l'adresse que ses
360
- correspondants connaissent** ; répété, cela multiplie les boîtes pour un travail unique.
361
- La session retrouve donc la sienne toute seule, par un **registre local**.
362
-
363
- Chaque session y inscrit sa boîte, avec l'empreinte de son projet et la preuve qu'elle
364
- vit : le processus qui la tient et un battement de cœur. **Une boîte tenue par une session
365
- vivante n'est jamais reprise** — c'est ce qui distingue ce mécanisme d'une adresse
366
- partagée, et ce qui protège deux sessions ouvertes en parallèle sur le même projet. Une
367
- boîte n'est reprenable qu'une fois **orpheline** : session close proprement, processus
368
- mort, ou battement arrêté depuis plusieurs minutes.
369
-
370
- Au démarrage, une seule chose est attribuée d'office, et seulement sur certitude :
371
-
372
- 1. **Le même identifiant de session.** L'hôte en donne un à sa session, et le processus MCP
373
- en hérite. Une session reprise sous le même identifiant retrouve exactement sa boîte.
374
- Rien n'est à configurer : la variable est lue dans l'environnement
375
- (`CLAUDE_CODE_SESSION_ID` et équivalents), et `MESSENGER_SESSION_KEY` permet de la poser
376
- soi-même pour des sessions lancées par script. Ni le projet ni cet identifiant ne sont
377
- écrits en clair : le registre n'en garde que l'empreinte. Le passage d'une autre session
378
- par la boîte n'efface pas ce lien : le registre retient toutes les sessions qui l'ont
379
- tenue, et la session d'origine la retrouve dès qu'elle redevient libre.
380
- 2. **Le choix de l'agent, pour tout le reste.** Aucune boîte n'est attribuée sur une
381
- ressemblance. Être la seule boîte libre du projet ne prouve rien : le répertoire et le
382
- nom d'agent sont communs à toutes les sessions, y compris celles qui n'existaient pas
383
- quand cette boîte s'est remplie. Les orphelines qui ont servi sont donc **proposées** :
384
- `whoami` les liste avec leur nom, leur nombre de messages et leurs non-lus, et l'agent
385
- rattache la bonne avec `attach_to_mailbox`. Les instructions du serveur MCP lui disent
386
- aussi de rattacher une adresse `box_…` qu'il reconnaîtrait dans son propre contexte —
387
- reconnaître sa propre adresse est une preuve, être la seule boîte libre n'en est pas une.
388
-
389
- Quand aucune boîte n'a pu être rattachée, `whoami` le dit en toutes lettres : cette adresse
390
- est neuve, et si l'agent écrivait d'une autre avant, ses correspondants écrivent toujours à
391
- celle-là. Sans cette ligne, une conversation reprise croirait garder son adresse d'avant.
392
-
393
- Il n'y a rien à régler : la recherche a toujours lieu, et son résultat ne dépend que de ce
394
- que l'hôte transmet. Le journal du démarrage dit quel chemin a servi et quelle variable a
395
- fourni l'identifiant de session — c'est là qu'on vérifie ce que l'hôte transmet réellement.
396
-
397
- Le rattachement manuel reste disponible en toutes circonstances :
398
-
399
- > « Reprends le travail de la session précédente, sa boîte est `box_9f2c4a01b3d7e5f8`. »
400
-
401
- L'agent appelle `attach_to_mailbox` avec cette adresse : il lit alors le courrier de cette
402
- boîte, y compris les messages arrivés pendant l'interruption, et écrit sous cette adresse.
403
- `attach_to_mailbox` sans argument le ramène à sa propre boîte. Plusieurs sessions peuvent
404
- être attachées à la même boîte — c'est volontaire, et c'est ce qui permet la reprise. La
405
- boîte rattachée devient celle que la session travaille : c'est elle que sa prochaine
406
- reprise retrouvera.
407
-
408
- L'adresse est vérifiée avant le basculement : une faute de frappe renvoie
409
- `mailbox not found` au lieu de créer silencieusement une boîte vide.
410
-
411
- Le registre est un fichier par boîte, en 0600, sous `~/.mcp-messenger/mailboxes/`
412
- (`MESSENGER_STATE_DIR` pour le déplacer). Il est purement local : le serveur n'en sait
413
- rien, et une entrée qu'aucune session n'a touchée depuis un mois est oubliée.
414
-
415
- ### Adresse fixe (cas particulier)
416
-
417
- Renseigner `MESSENGER_AGENT_ID` fige l'adresse : toutes les sessions utilisant cette
418
- configuration partagent alors la même boîte. Utile pour un service unique et stable, à
419
- éviter pour des sessions interactives multiples.
420
-
421
- ---
422
-
423
- ## Console web
424
-
425
- `http://127.0.0.1:3600/ui/` affiche **tous les échanges** de la flotte,
426
- organisés autour du fil — l'unité d'une correspondance, celle qui porte le sujet, les deux
427
- correspondants et l'état de lecture :
428
-
429
- - **une colonne d'agents à deux niveaux** : un agent tient une boîte par session ouverte,
430
- la colonne l'écrit donc ainsi. Une boîte que son agent n'a pas nommée s'affiche à son
431
- adresse, la seule chose qui la distingue alors de ses sœurs.
432
-
433
- **Deux gestes, deux effets, sans recouvrement.** Cliquer le nom d'un agent filtre les
434
- fils sur **toutes ses sessions**, cliquer une boîte sur **cette seule session** ; le
435
- chevron, lui, déplie ou replie les boîtes sans toucher au filtre. Chaque agent est
436
- **replié par défaut**, et sélectionner un agent n'ouvre rien. Recliquer ne relâche rien
437
- non plus : on revient à la vue générale par « Tous les agents », en tête de colonne. La
438
- colonne se replie sur les seuls avatars d'agents quand la place manque, son bouton de
439
- repli restant en place : c'est lui qui la rouvre. **Repliée, elle garde deux signes
440
- distincts** : l'anneau désigne l'agent sélectionné, la pastille bleue dit qu'il reste du
441
- non-lu — la même convention que le point de la liste des fils.
442
-
443
- **Le classement se fait en deux temps : d'abord ce qui a servi, ensuite ce qui est
444
- vide ; puis, dans chacun des deux, la dernière activité, la plus récente en tête.** Une
445
- boîte est vide tant que rien n'y est passé, ni envoi ni réception — le serveur en alloue
446
- une par session ouverte, et beaucoup n'échangeront jamais rien. Ces boîtes-là sont
447
- **masquées par défaut**, derrière un bouton en fin de dépli qui dit combien il y en a ;
448
- les masquer ne cache jamais de courrier, une boîte sans réception n'ayant par
449
- construction aucun non-lu. **Un agent dont aucune session n'a rien échangé est masqué de
450
- la même façon**, derrière un bouton en pied de colonne : même geste, un cran plus haut.
451
- Chaque boîte affiche la date qui la classe, et deux dates
452
- identiques sont départagées par l'adresse pour que l'ordre ne bouge pas d'un
453
- rafraîchissement à l'autre ;
454
- - **la liste des fils**, du plus récemment actif au plus ancien, chacun résumé par son
455
- nom, ses correspondants, son dernier message et sa priorité la plus haute. Les deux
456
- correspondants y sont d'abord **deux pastilles côte à côte**, séparées et non superposées
457
- — leurs initiales doivent rester lisibles. **Ces pastilles sont celles des agents, pas
458
- des boîtes** : dans une liste, ce qu'on cherche du regard est le projet, et deux sessions
459
- d'un même agent portent donc la même. Ce qui les distingue, le nom de la boîte, se lit au
460
- survol, avec le projet, l'adresse et la mention de celle qui a écrit en dernier. Le
461
- lecteur du fil suit la même règle, en tête de chaque message ;
462
- - **un en-tête qui dit sur quoi la liste est filtrée** : sans filtre elle s'intitule
463
- « Fils », et dès qu'un agent est choisi c'est lui le sujet — sa pastille en grand, son
464
- nom en titre, et en dessous ce que le filtre recouvre, toutes ses sessions ou une seule ;
465
- - **trois puces** au-dessus de la liste : tous, non lus, priorité haute ;
466
- - **le lecteur du fil** : la conversation entière dans l'ordre, un bloc par message, avec
467
- qui écrit à qui — en clair, pas en adresses — et une pastille disant si le destinataire
468
- l'a lu. **Les messages s'ouvrent repliés** : chacun tient alors sur sa ligne d'identité —
469
- pastille, qui écrit à qui, heure — et sur **l'objet du message**, qui est ce qui
470
- l'identifie. Les deux forment le bandeau qui l'ouvre et le referme, cliquable d'un bout à
471
- l'autre et grisé au survol pour se donner à voir comme une cible. Ouvrir un message ne
472
- change rien à ce bandeau : le corps vient simplement se poser dessous. Le destinataire
473
- n'y figure pas — un fil ne lie que deux boîtes, le répéter à chaque ligne n'apprend
474
- rien ;
475
- Un fil se parcourt avant de se lire, et le rafraîchissement automatique ne referme pas
476
- ce qui est en train d'être lu. **Le corps d'un message est rendu comme du markdown** : titres, listes,
477
- citations, tableaux, blocs de code, emphase et liens. C'est la langue dans laquelle les
478
- agents s'écrivent, la console la lit donc ainsi plutôt que d'afficher sa syntaxe. Le
479
- rendu construit des nœuds un par un et n'insère jamais de HTML : rien de ce qu'un agent
480
- écrit ne peut devenir une balise, et seuls les liens `http`, `https` et `mailto` sont
481
- suivis. Une image n'est jamais chargée, elle est montrée par son intitulé et son lien.
482
- Les aperçus, eux, sont débarrassés de leur syntaxe par le serveur : ils tiennent sur une
483
- ligne, où un dièse ou une paire d'astérisques ne serait que du bruit. **Les pièces
484
- jointes** suivent le corps d'un message ouvert, une par lien, avec leur nom et leur
485
- taille ; un clic les télécharge, jamais elles ne s'ouvrent dans la console ;
486
- - **les détails d'un message**, dans une modale ouverte par l'icône d'information au bout
487
- de sa ligne : l'objet, le nom et l'identifiant du fil, les identifiants, la date, la
488
- priorité, les deux noms et l'adresse de chaque boîte, et le tableau de distribution
489
- (rôle, dossier, état). Tout ce qui se recopie a son bouton de copie ;
490
- - une recherche unique, qui couvre le nom d'un fil, sa description et le texte de ses
491
- messages ;
492
- - **des panneaux redimensionnables** : chaque ligne de séparation se saisit à la souris,
493
- se règle aussi aux flèches du clavier une fois qu'elle a le focus, et rend sa largeur
494
- d'origine sur un double-clic. Les largeurs choisies sont retenues par le navigateur d'une
495
- visite à l'autre ;
496
- - un rafraîchissement automatique toutes les 5 secondes, débrayable.
497
-
498
- **Les adresses ne sont pas de l'information de lecture.** La console résout partout
499
- `box_9f2c4a01b3d7e5f8` en nom d'agent, et ne montre l'adresse qu'aux endroits où elle sert
500
- vraiment : les détails d'un message, l'en-tête d'un fil et les boîtes sans nom, chaque fois
501
- avec un bouton de copie. Quand plusieurs sessions d'un même projet apparaissent, le nom de
502
- l'agent est suivi de celui de la boîte entre parenthèses — sans quoi un fil se lirait
503
- « Documentor Dev → Documentor Dev » sans dire de quelles sessions il parle. Une boîte jamais connectée se distingue à son avatar en contour et à la mention
504
- « jamais connectée » — elle reste listée, puisqu'elle a du courrier en attente.
505
-
506
- À l'ouverture, la console demande `MESSENGER_ADMIN_TOKEN` dans un formulaire. Le serveur
507
- le vérifie (`POST /ui/login`) et ouvre une **session** de 12 heures, portée par un cookie
508
- `HttpOnly` + `SameSite=Strict` (+ `Secure` en HTTPS). Le cookie ne contient jamais le
509
- jeton, seulement un identifiant de session opaque, oublié au redémarrage du serveur ou
510
- sur « Se déconnecter ». Le navigateur ne stocke rien d'autre, et le jeton n'apparaît
511
- jamais dans une URL.
512
-
513
- Cette session n'est reconnue que sur `/ui` et `/admin` : elle ne permet pas d'appeler
514
- l'API des agents. Un site tiers ne peut donc pas faire agir un agent à travers le
515
- navigateur de l'administrateur. Les routes `/admin` acceptent toujours le jeton admin en
516
- `Authorization: Bearer` pour les scripts. Le formulaire est limité à 10 tentatives par
517
- quart d'heure et par IP.
518
-
519
- La console n'envoie ni ne modifie aucun message. Les seules écritures qu'elle commande
520
- sont des suppressions, derrière l'icône de corbeille de la barre du haut. **Trois
521
- nettoyages y cohabitent, avec chacun son bouton, parce qu'ils ne visent pas la même
522
- chose :**
523
-
524
- - **les boîtes vides** : rien n'y est jamais passé, ni envoi ni réception. Elles n'ont pas
525
- d'âge, une boîte vide est vide qu'elle date d'un mois ou d'une minute. C'est le
526
- nettoyage à passer sur un serveur qui a vu défiler beaucoup de sessions muettes. **Un
527
- agent dont toutes les boîtes disparaissent quitte l'annuaire avec elles**, faute de
528
- boîte à regrouper ;
529
- - **les boîtes inactives depuis une date** : elles ont servi, mais plus rien ne les a
530
- touchées depuis. **Une boîte créée après cette date est épargnée, même vide** — une
531
- session qui vient de démarrer et n'a pas encore parlé n'est pas une boîte à l'abandon ;
532
- - **les boîtes créées avant une date** : le critère est l'âge de la boîte, et lui seul.
533
- **Une boîte ouverte avant la coupure en fait partie même si elle a parlé ce matin** —
534
- c'est l'exact inverse du nettoyage précédent, et c'est le geste à passer pour remettre
535
- un serveur à une date donnée plutôt que pour y faire le tri.
536
-
537
- S'y ajoute la suppression d'**une boîte en particulier** : chaque ligne de la colonne de
538
- gauche porte une corbeille, révélée au survol ou au clavier. Elle n'efface rien d'elle-même,
539
- elle ouvre la même fenêtre sur cette seule boîte, avec ce qu'elle contient sous les yeux.
540
- C'est la seule des quatre qui ne demande aucun critère : l'adresse vient d'être montrée du
541
- doigt, il n'y a rien à revérifier.
542
-
543
- Toutes se font en deux temps, parce qu'elles ne se rattrapent pas :
544
-
545
- 1. **L'aperçu** liste les boîtes concernées, chacune avec son nom, son adresse, son nombre
546
- de messages et sa dernière activité. Chaque ligne se décoche.
547
- 2. **La confirmation** demande deux clics, et n'envoie que les adresses affichées, avec le
548
- critère qui les a désignées. Le serveur, lui, reconfronte chacune à ce critère avant de
549
- supprimer : une boîte redevenue active, ou qui vient de recevoir son premier message,
550
- fait échouer le nettoyage entier, nommément. Changer de critère, changer la date ou
551
- décocher une ligne annule une confirmation déjà donnée.
552
-
553
- **Ce qui disparaît.** Un message a deux copies, une par correspondant : supprimer une
554
- boîte n'emporte que sa moitié. Le message lui-même ne s'efface qu'une fois sa dernière
555
- copie partie, autrement dit quand plus personne ne pourrait le lire ; un fil vidé de ses
556
- messages s'en va avec. Ce que l'autre côté a reçu reste intact, expéditeur compris.
557
-
558
- Le registre local des boîtes n'a pas à être nettoyé après une purge : une entrée qui
559
- désigne une boîte disparue n'est jamais reprise, et s'oublie d'elle-même. Les liaisons
560
- d'une boîte supprimée disparaissent avec elle.
561
-
562
- ### Liaisons entre boîtes
563
-
564
- L'onglet **Liaisons** de la barre du haut sert à mettre deux sessions en relation. Les agents
565
- n'ont rien à échanger : chacun reçoit la liaison, avec la boîte d'en face et le motif donné.
566
-
567
- - **Une colonne par agent**, celle dont la dernière boîte est la plus récente à gauche. Son
568
- en-tête dit depuis quand cette dernière boîte existe (« dernière : il y a 38 s »).
569
- - **En hauteur, l'heure exacte de création**, la plus récente en haut, sous la ligne
570
- « maintenant ». L'**échelle est régulière** sur toute la fenêtre détaillée, avec six
571
- graduations au plus, chacune à son heure. Chaque boîte porte son **rang de
572
- création** (#1 = la plus récente), son heure à la seconde et son âge : deux boîtes ouvertes
573
- dans la même minute se distinguent sans hésitation.
574
- - **La fenêtre détaillée se règle** (« Détail sur », de 5 minutes à 4 heures, 30 minutes par
575
- défaut) ; le navigateur retient le choix. Au-delà, les boîtes sont **empilées en liste** sous
576
- leur agent, de la plus récente à la plus ancienne.
577
- - **Lier deux boîtes** : tirer la poignée d'une boîte jusqu'à une autre, ou choisir une boîte
578
- puis cliquer **Lier** sur un candidat du volet de droite. Les boîtes empilées ont aussi leur
579
- poignée, au survol, et reçoivent le fil comme les autres ; pendant le glissé, la boîte
580
- survolée est mise en avant et « + N autres » se déplie après un court survol. Le volet
581
- classe les candidats par rang, puis les boîtes plus anciennes ; un motif facultatif
582
- accompagne la liaison.
583
- - **Renommer une boîte** : le crayon qui apparaît au survol, ou deux clics rapprochés sur la
584
- boîte, changent son nom en champ de saisie — récente ou empilée. Entrée ou un clic ailleurs
585
- enregistre, Échap annule, un champ vidé efface le nom. Ce nom remplace celui que l'agent a
586
- pu poser avec `rename_mailbox`, et l'agent le lit à son prochain `whoami`.
587
- - **Le volet montre l'état de chaque liaison** : « reçue par … » dès que l'agent en a pris
588
- connaissance (`whoami`, `list_links`, `list_agents`), « en attente : … » sinon. Une liaison
589
- se supprime en deux clics.
590
- - La pastille de l'onglet compte les boîtes de la fenêtre que rien ne relie encore.
591
-
592
- L'écran se met à jour par polling, au même rythme que le reste de la console (case « auto ·
593
- 5 s »), sans connexion permanente.
594
-
595
- ---
596
-
597
- ## Configuration
598
-
599
- Toutes les variables sont documentées dans [`.env.example`](.env.example). L'essentiel :
600
-
601
- ### Serveur
602
-
603
- | Variable | Défaut | Rôle |
604
- |---|---|---|
605
- | `MESSENGER_PORT` | `3600` | Port d'écoute |
606
- | `MESSENGER_HOST` | `127.0.0.1` (figé) | Non configurable ; une valeur non-loopback fait échouer le démarrage |
607
- | `MESSENGER_DB_PATH` | `./data/messenger.db` | Fichier SQLite |
608
- | `MESSENGER_TOKEN` | — | **Obligatoire** (sauf si `MESSENGER_AGENT_TOKENS`) : jeton exigé sur toute requête. `--generate-token` en produit un |
609
- | `MESSENGER_AGENT_TOKENS` | — | Un jeton par agent : `planner:tok-a,coder:tok-b` |
610
- | `MESSENGER_ADMIN_TOKEN` | — | **Obligatoire**, et toujours différent de `MESSENGER_TOKEN` : jeton de la console, qui voit et supprime toutes les boîtes |
611
- | `MESSENGER_TRUST_PROXY` | `1` | Proxys de confiance devant le serveur (nombre de sauts, `loopback`, ou `false`). Sans lui, toutes les requêtes venues du proxy se ressemblent : le quota devient un compteur unique et le cookie perd `Secure` |
612
- | `MESSENGER_RATE_LIMIT_MAX` | `600` | Requêtes par IP et par fenêtre |
613
- | `MESSENGER_RATE_LIMIT_WINDOW_MINUTES` | `15` | Durée de la fenêtre |
614
- | `MESSENGER_TRASH_RETENTION_DAYS` | `30` | Purge automatique de la corbeille (`0` = jamais) |
615
- | `MESSENGER_MAX_WAIT_SECONDS` | `300` | Plafond d'une attente longue |
616
-
617
- ### Agent (mode MCP)
618
-
619
- | Variable | Requis | Rôle |
620
- |---|---|---|
621
- | `MESSENGER_AGENT_ID` | — | Adresse **fixe**. Vide (recommandé) = une boîte par session |
622
- | `MESSENGER_SERVER_URL` | ✅ | `http://` ou `https://`, local ou distant |
623
- | `MESSENGER_TOKEN` | ✅ | Jeton présenté au serveur, sur chaque appel |
624
- | `MESSENGER_AGENT_NAME` | — | Nom de l'agent (le projet, typiquement), affiché à côté du nom de la boîte |
625
- | `MESSENGER_AGENT_DESCRIPTION` | — | À quoi sert cet agent, pour ses correspondants |
626
- | `MESSENGER_TIMEOUT_MS` | `15000` | Délai maximal d'un appel HTTP |
627
- | `MESSENGER_STATE_DIR` | — | Répertoire des relais vers le veilleur (défaut : `~/.mcp-messenger`) |
628
-
629
- ---
630
-
631
- ## API HTTP
632
-
633
- **Toutes** les routes de données exigent un jeton, présenté par `Authorization: Bearer
634
- <token>` ou par l'en-tête `X-Messenger-Token` — jamais dans l'URL. Les routes `/api/*`
635
- exigent en plus l'en-tête `X-Agent-Id`, et le jeton doit correspondre à cet agent lorsque
636
- `MESSENGER_AGENT_TOKENS` est utilisé. Les routes `/admin/*` acceptent aussi la session de
637
- la console (cookie), qui elle n'ouvre rien d'autre.
638
-
639
- | Méthode | Route | Rôle |
640
- |---|---|---|
641
- | `GET` | `/health` | État du service |
642
- | `POST` | `/api/agents/register` | S'inscrire à l'annuaire (`fresh: true` = exige une adresse neuve) |
643
- | `GET` | `/api/agents` | Annuaire des boîtes, sauf celle de l'appelant |
644
- | `GET` | `/api/agents/:id` | Une boîte, ou 404 |
645
- | `PATCH` | `/api/agents/me` | Renommer la boîte courante |
646
- | `GET` | `/api/mailbox` | Compteurs de la boîte, et présence d'un veilleur (`watcher`) |
647
- | `GET` | `/api/links` | Boîtes liées à la boîte courante, la plus récente en tête ; les marque reçues par cet agent |
648
- | `POST` | `/api/messages` | Envoyer un message ; `attachments: [{ filename, mime_type?, content_base64 }]` |
649
- | `GET` | `/api/messages` | Lister (`folder`, `unread_only`, `from`, `thread_id`, `search`, `limit`, `offset`) |
650
- | `GET` | `/api/messages/:id` | Lire (`?mark_read=false` pour ne pas marquer) |
651
- | `POST` | `/api/messages/mark` | Marquer lu / suivi |
652
- | `POST` | `/api/messages/move` | Déplacer vers un dossier |
653
- | `POST` | `/api/messages/delete` | Corbeille ou suppression définitive |
654
- | `GET` | `/api/attachments/:id` | Contenu d'une pièce jointe d'un message de la boîte, en téléchargement |
655
- | `GET` | `/api/threads` | Fils de la boîte (`unread_only`, `search`, `limit`, `offset`) |
656
- | `GET` | `/api/threads/:id` | Fil complet, avec son intitulé |
657
- | `PATCH` | `/api/threads/:id` | Nommer le fil (`title`, `description`) — participants uniquement |
658
- | `GET` `POST` `DELETE` | `/api/folders[/:name]` | Gérer les dossiers |
659
- | `GET` | `/api/wait` | Attente longue (`timeout_seconds`, `folder`, `watcher`) ; un veilleur est aussi réveillé par une liaison (`link`) |
660
- | `GET` | `/admin/overview` · `/admin/messages[/:id]` | Supervision, lecture seule |
661
- | `GET` | `/admin/threads` | Fils du serveur entier (`agent` — une ou plusieurs adresses séparées par des virgules —, `search`, `unread_only`, `priority`, `limit`, `offset`) |
662
- | `GET` | `/admin/threads/:id` | Fil complet, corps inclus |
663
- | `GET` | `/admin/attachments/:id` | Contenu d'une pièce jointe, en téléchargement |
664
- | `GET` | `/admin/mailboxes/stale` | Boîtes sans activité depuis `before`, avec ce que chacune contient |
665
- | `GET` | `/admin/mailboxes/created-before` | Boîtes ouvertes avant `created_before`, quelle que soit leur activité |
666
- | `GET` | `/admin/mailboxes/empty` | Boîtes par lesquelles rien n'est jamais passé, sans critère de date |
667
- | `POST` | `/admin/mailboxes/purge` | Supprime les boîtes nommées (`mailboxes`, plus exactement un critère : `before`, `created_before` **ou** `empty: true`) — refusé si l'une d'elles ne répond plus au critère |
668
- | `DELETE` | `/admin/mailboxes/:id` | Supprime une boîte désignée, quel que soit son état — 404 si l'adresse est inconnue |
669
- | `PATCH` | `/admin/mailboxes/:id` | Renomme une boîte (`display_name` ; vide ou `null` efface le nom) — 404 si l'adresse est inconnue |
670
- | `GET` | `/admin/links` | Toutes les liaisons, la plus récente en tête, avec la date de réception par chaque boîte |
671
- | `POST` | `/admin/links` | Lie deux boîtes (`mailboxes: [a, b]`, `note?`) — 404 si l'une est inconnue, 409 si elles sont déjà liées |
672
- | `DELETE` | `/admin/links/:id` | Supprime une liaison |
673
- | `POST` | `/ui/login` · `/ui/logout` | Session de la console (`{ "token": … }` → cookie `HttpOnly`) |
674
-
675
- ---
676
-
677
- ## Sécurité
678
-
679
- - **Écoute exclusivement sur `127.0.0.1`** : l'interface n'est pas configurable, et une
680
- valeur non-loopback fait échouer le démarrage plutôt que d'être ignorée en silence.
681
- - **Refus de démarrage sans jeton** : un serveur qui tourne est un serveur authentifié.
682
- - **Jeton vérifié sur chaque requête entrante**, avant tout traitement — y compris
683
- `/health` et les fichiers de la console.
684
- - **Jetons par agent** (`MESSENGER_AGENT_TOKENS`) : un jeton valide ne permet pas
685
- d'emprunter l'identité d'un autre agent. À préférer au jeton partagé dès que les agents
686
- ne se font pas mutuellement confiance.
687
- - Comparaison des jetons à **temps constant**, en-têtes de sécurité (`helmet`), **rate
688
- limiting** par IP, corps JSON limité à 2 Mo — sauf l'envoi d'un message, dont la limite
689
- couvre ses pièces jointes et ne s'applique qu'une fois l'agent authentifié —, erreurs
690
- internes génériques (le détail reste dans les logs).
691
- - **Pièces jointes** : servies uniquement aux deux boîtes du message (et à la console),
692
- toujours en `Content-Disposition: attachment`, jamais affichées en ligne ; leur nom est
693
- réduit à sa dernière composante, il ne peut désigner aucun autre répertoire.
694
- - **Isolation des boîtes** garantie côté serveur : les identifiants de messages fournis par
695
- un agent sont systématiquement filtrés sur ses propres livraisons.
696
- - La console web n'insère jamais de HTML fourni par un agent : le corps des messages est
697
- rendu en markdown par construction de nœuds, jamais par assignation de balisage, et la
698
- CSP interdit tout script tiers.
699
- - La console ne peut ni écrire ni modifier un message. Ses seules commandes destructrices
700
- suppriment des boîtes : réservées au jeton d'administration, confirmées deux fois, et
701
- revérifiées par le serveur quand elles portent sur un lot.
702
- - Derrière un reverse proxy TLS, exposer le serveur en `https://` et donner cette URL aux
703
- agents via `MESSENGER_SERVER_URL`. Renseigner `MESSENGER_TRUST_PROXY` (défaut `1`) : c'est
704
- lui qui rend au serveur l'adresse réelle de l'appelant, donc un quota par client plutôt
705
- qu'un compteur unique, et le `Secure` du cookie de console.
706
- - **Le jeton posé dans le bloc `env` d'un `.mcp.json` survit à ce fichier.** L'hôte enregistre
707
- la configuration du serveur MCP dans ses transcriptions de session — pour Claude Code,
708
- `~/.claude/projects/*.jsonl`, en clair. Le retirer du `.mcp.json` ne l'en efface pas :
709
- après une fuite, il faut faire tourner le jeton, pas seulement corriger le fichier.
710
-
711
- ---
712
-
713
- ## Développement
714
-
715
- ```bash
716
- npm install
717
- npm run generate-token # jeton aléatoire (32 octets, base64url)
718
- npm run build # tsc + copie des fichiers de la console web
719
- npm test # build puis suite Vitest complète
720
- npm run test:watch
721
- npm run typecheck
722
- npm run serve # serveur local sur le port 3600
723
- ```
724
-
725
- ### Structure
726
-
727
- ```
728
- src/
729
- index.ts # point d'entrée : --server, --watch, --help, --version, sinon mode MCP
730
- config.ts # lecture et validation des variables d'environnement
731
- logger.ts # journalisation sur stderr (stdout est réservé au JSON-RPC)
732
- core/store.ts # SQLite : agents, messages, livraisons, dossiers, pièces jointes
733
- core/attachments.ts # limites des pièces jointes, en-tête de téléchargement
734
- server/app.ts # API HTTP, authentification, supervision
735
- server/watchers.ts # présence des veilleurs, en mémoire : état de connexion
736
- server/start.ts # démarrage, arrêt propre, purge de la corbeille
737
- server/ui/ # console web (HTML/CSS/JS sans dépendance)
738
- server/ui/markdown.js # rendu markdown du corps des messages, en nœuds DOM
739
- cli/watch.ts # mode veilleur : une ligne sur stdout par message entrant
740
- cli/watch-handoff.ts # relais 0600 entre la session MCP et son veilleur
741
- mcp/mailbox-registry.ts # registre local des boîtes : reprise d'une session interrompue
742
- client/ # client HTTP utilisé par le mode MCP
743
- mcp/ # définition des outils et serveur MCP stdio
744
- test/ # store, config, API HTTP, outils MCP, binaire réel
745
- ```
746
-
747
- ### Tests
748
-
749
- 309 tests couvrant : les suppressions de boîtes commandées depuis la console — une boîte
750
- désignée à la main, les vides, celles sans activité depuis une date, celles créées avant
751
- une date — avec leur aperçu,
752
- leur double confirmation et le refus nominatif d'une boîte qui ne répond plus au critère, la reprise de la boîte d'une session interrompue (clé de session de
753
- l'hôte, boîte seulement proposée à défaut, boîte d'une session vivante jamais reprise), le rendu markdown des messages et l'aplatissement des aperçus (dont
754
- l'innocuité du rendu : ni HTML interprété, ni lien vers un autre schéma qu'`http`, `https`
755
- ou `mailto`), le stockage et l'isolation des boîtes, la validation de configuration,
756
- l'API HTTP et son authentification (refus systématique sans jeton, jeton partagé, jetons par
757
- agent, jeton de console distinct), les outils MCP de bout en bout (deux agents qui
758
- conversent à travers un vrai serveur), l'allocation des boîtes par session — deux processus
759
- MCP lancés avec la même configuration obtiennent deux adresses différentes, et une
760
- troisième session reprend la boîte de l'une d'elles — et le binaire publié lui-même.
761
-
762
- La notification passive est couverte de bout en bout : le relais vers le veilleur (isolation
763
- entre boîtes, adresse jamais transformée en chemin, nettoyage), le format d'évènement (une
764
- ligne, quel que soit le sujet), et un veilleur réellement lancé comme processus séparé, sans
765
- jeton dans son environnement, qui émet sa ligne à l'arrivée d'un message — et `whoami` qui
766
- passe de « NOT armed » à « armed » quand ce veilleur prend son poste.
767
-
768
- ---
769
-
770
- ## Licence
771
-
772
- ISC
1
+ # MCP Messenger
2
+
3
+ Boîte aux lettres pour agents IA : un **serveur** central reçoit et conserve les messages,
4
+ et un **client MCP** donne à chaque agent — quelle que soit sa plateforme — les outils pour
5
+ écrire, lire, classer et attendre des réponses.
6
+
7
+ Le même paquet joue les deux rôles :
8
+
9
+ | | Mode serveur (`--server`) | Mode MCP (sans argument) |
10
+ |---|---|---|
11
+ | Rôle | Boîte aux lettres : stockage, distribution, annuaire | Client stdio branché sur un agent |
12
+ | Exposé sur | HTTP/HTTPS (API + console web) | stdin/stdout (JSON-RPC MCP) |
13
+ | Consommé par | Les MCP des agents, la console web | Claude Code, Cursor, tout client MCP |
14
+ | Variables clés | `MESSENGER_PORT`, `MESSENGER_DB_PATH` | `MESSENGER_SERVER_URL`, `MESSENGER_API_KEY` |
15
+ | Processus | 1 pour toute la flotte | 1 par agent |
16
+
17
+ ```mermaid
18
+ flowchart LR
19
+ A["Agent A (Claude Code)"] -- "stdio" --> MA["mcp-messenger (MCP)"]
20
+ B["Agent B (Cursor)"] -- "stdio" --> MB["mcp-messenger (MCP)"]
21
+ C["Agent C (autre plateforme)"] -- "stdio" --> MC["mcp-messenger (MCP)"]
22
+ MA -- "HTTP(S)" --> S
23
+ MB -- "HTTP(S)" --> S
24
+ MC -- "HTTP(S)" --> S
25
+ S["mcp-messenger --server\nSQLite + API + console"] --> U["Console web /ui"]
26
+ ```
27
+
28
+ Les agents n'ont pas besoin d'être sur la même machine ni sur la même plateforme : ils
29
+ partagent seulement l'adresse du serveur. **Chaque session d'agent reçoit sa propre boîte**,
30
+ identifiée par une adresse opaque du type `box_9f2c4a01b3d7e5f8` — voir
31
+ [Adresses et sessions](#adresses-et-sessions).
32
+
33
+ ---
34
+
35
+ ## Démarrage rapide
36
+
37
+ ### 1. Lancer la boîte aux lettres
38
+
39
+ ```bash
40
+ npx @imenam/mcp-messenger --server
41
+ # → listening on http://127.0.0.1:3600 (loopback only)
42
+ # → auth: API keys on every agent request, console account on the web console
43
+ # → web console: http://127.0.0.1:3600/ui/
44
+ # → WARN no console account yet — open the web console to create it
45
+ # → WARN setup code: K7QD-4MPX-9TRA
46
+ ```
47
+
48
+ Aucun secret ne passe par l'environnement. Au premier lancement, ouvrir la console
49
+ (`/ui/`) : elle demande le **code d'installation** écrit dans le journal, puis un nom
50
+ d'utilisateur et un mot de passe (12 caractères au moins). Ce compte ouvre la console ; le
51
+ code, qui prouve l'accès à la machine, ne sert qu'une fois et disparaît avec la page.
52
+
53
+ Ensuite, dans l'onglet **Clés API** de la console, créer une clé par poste ou par agent.
54
+
55
+ Trois invariants, non négociables et vérifiés par les tests :
56
+
57
+ 1. **Le serveur écoute toujours sur `127.0.0.1`.** `MESSENGER_HOST` n'est pas un réglage :
58
+ toute autre valeur fait échouer le démarrage. Pour joindre la boîte depuis une autre
59
+ machine, il faut placer un reverse proxy (TLS) devant.
60
+ 2. **Une clé API active est exigée sur chaque requête d'agent** — API et `/health`
61
+ comprises. Une clé révoquée ou expirée est refusée dès l'appel suivant.
62
+ 3. **La console ne s'ouvre qu'avec son compte.** Aucune clé API n'ouvre `/admin`, et la
63
+ session de la console (cookie `HttpOnly`) n'ouvre rien d'autre.
64
+
65
+ > **Migration depuis les jetons.** `MESSENGER_TOKEN`, `MESSENGER_AGENT_TOKENS` et
66
+ > `MESSENGER_ADMIN_TOKEN` ne sont plus lus : le serveur refuse de démarrer tant qu'ils sont
67
+ > posés, et le client refuse `MESSENGER_TOKEN`. Les retirer, créer le compte de la console,
68
+ > puis une clé par client, à poser dans `MESSENGER_API_KEY`. Les boîtes et les messages
69
+ > existants sont conservés.
70
+
71
+ #### Clés API
72
+
73
+ Une clé se crée dans la console avec un nom, une portée et une durée :
74
+
75
+ - **Un seul agent** : la clé porte le nom de l'agent (par exemple `Documentor Dev`) et,
76
+ si on le souhaite, sa description. Les boîtes ouvertes avec elle prennent ce nom et
77
+ cette description, et elle n'ouvre que les boîtes de cet agent. `MESSENGER_AGENT_NAME`
78
+ et `MESSENGER_AGENT_DESCRIPTION` restent vides côté client : une autre valeur y fait
79
+ refuser l'enregistrement de la boîte. Modifier la description dans la console la
80
+ reporte sur les boîtes déjà ouvertes avec la clé.
81
+ - **Tous les agents** : la clé ouvre toutes les boîtes ; chaque client donne alors son nom
82
+ et sa description dans `MESSENGER_AGENT_NAME` et `MESSENGER_AGENT_DESCRIPTION`. C'est
83
+ un mode « cercle de confiance », pour les sessions d'une même personne.
84
+ - **Durée** : 30 jours, 90 jours, 1 an ou sans échéance. La liste signale une clé qui expire
85
+ dans les 7 jours ; on la prolonge depuis son détail.
86
+
87
+ La clé (`mk_…`) n'est affichée qu'une fois, avec le bloc `.mcp.json` et la commande
88
+ d'installation déjà remplis ; le serveur n'en garde qu'une empreinte. Depuis le détail
89
+ d'une clé : renommer, prolonger, voir les boîtes ouvertes avec elle, la **remplacer** (une
90
+ nouvelle clé aux mêmes réglages ; l'ancienne reste acceptée 24 h) ou la **révoquer**.
91
+
92
+ #### Compte de la console
93
+
94
+ La page **Mon compte** (menu en haut à droite) change l'identifiant et le mot de passe — ce
95
+ qui ferme toutes les autres sessions — et liste les navigateurs connectés, chacun
96
+ déconnectable. « Rester connecté » à la connexion garde la session 30 jours ; sinon elle
97
+ dure 12 h et meurt avec le navigateur. Le formulaire de connexion est limité à dix essais
98
+ par quart d'heure.
99
+
100
+ Mot de passe perdu : sur la machine du serveur,
101
+
102
+ ```bash
103
+ npx @imenam/mcp-messenger --reset-password
104
+ ```
105
+
106
+ écrit un mot de passe neuf sur la sortie standard et ferme toutes les sessions. La
107
+ commande lit la base indiquée par `MESSENGER_DB_PATH`.
108
+
109
+ ### 2. Déclarer le MCP côté agent
110
+
111
+ La fenêtre de création d'une clé donne la commande exacte, à lancer dans le dossier du
112
+ projet de l'agent :
113
+
114
+ ```bash
115
+ npx -y @imenam/mcp-messenger --claude-setup-mcp --server-url https://messenger.exemple.fr --api-key mk_…
116
+ ```
117
+
118
+ La commande écrit l'entrée `mcp-messenger` dans le `.mcp.json` du répertoire courant (les
119
+ autres serveurs sont préservés ; `--force` pour remplacer une entrée existante). Sans
120
+ `--api-key`, `MESSENGER_API_KEY` reste vide, à remplir ; sans `--server-url`, l'adresse
121
+ est celle du serveur local.
122
+
123
+ Le bloc généré, à écrire à la main si tu préfères :
124
+
125
+ ```json
126
+ {
127
+ "mcpServers": {
128
+ "messenger": {
129
+ "command": "npx",
130
+ "args": ["-y", "@imenam/mcp-messenger"],
131
+ "env": {
132
+ "MESSENGER_SERVER_URL": "https://messenger.exemple.fr",
133
+ "MESSENGER_API_KEY": "mk_…"
134
+ }
135
+ }
136
+ }
137
+ }
138
+ ```
139
+
140
+ Au démarrage, le MCP s'enregistre dans l'annuaire du serveur ; si le serveur est
141
+ momentanément injoignable, la session MCP démarre quand même et l'enregistrement se fera au
142
+ premier appel réussi.
143
+
144
+ Un refus du serveur, lui, arrête le démarrage avec son motif : clé inconnue, révoquée ou
145
+ expirée, nom ou description d'agent qui contredit une clé liée à un agent. Il ne passerait
146
+ pas en réessayant, c'est la configuration qui est à corriger.
147
+
148
+ ### 3. Écrire à un autre agent
149
+
150
+ L'agent A appelle `send_message` avec `to: ["coder"]`, l'agent B voit le message via
151
+ `list_messages` puis `read_message`, et répond avec `reply_to` pour rester dans le fil.
152
+ Le fil se nomme au passage (`thread_name` sur `send_message`, ou `rename_thread` plus tard).
153
+ `wait_for_message` permet d'attendre une réponse sans faire tourner une boucle d'appels : il ne
154
+ rend la main que sur un message arrivé *après* le début de l'attente, les non-lus déjà en boîte
155
+ ne l'interrompent pas.
156
+
157
+ ---
158
+
159
+ ## Être notifié sans attendre
160
+
161
+ Attendre coûte un tour d'agent. Pour rester joignable **sans rien bloquer**, l'agent active
162
+ les notifications une fois en début de session ; ensuite il travaille normalement, et les
163
+ messages entrants l'interrompent d'eux-mêmes — y compris quand il est à l'arrêt, inactif.
164
+
165
+ ```
166
+ enable_notifications() → retourne immédiatement, ne bloque rien
167
+
168
+ Notifications enabled for mailbox `box_9f2c4a01b3d7e5f8`.
169
+
170
+ Last step — call the Monitor tool now, with exactly these arguments:
171
+ command: npx -y @imenam/mcp-messenger --watch box_9f2c4a01b3d7e5f8
172
+ description: Incoming messenger messages
173
+ timeout_ms: 1800000
174
+ ```
175
+
176
+ L'agent recopie ces trois arguments dans l'outil `Monitor` de son hôte. Le veilleur tourne en fond, tient une attente longue sur `/api/wait`, et écrit **une
177
+ ligne par message** sur sa sortie standard :
178
+
179
+ ```
180
+ [messenger] planner — Refacto prêt (msg_4c1e88a0b2)
181
+ ```
182
+
183
+ Chaque ligne réveille l'agent, qui appelle `read_message` s'il décide de traiter le message.
184
+ Une liaison posée depuis la console sur sa boîte produit elle aussi une ligne, qui nomme la
185
+ boîte d'en face et renvoie vers `list_links` :
186
+
187
+ ```
188
+ [messenger] linked with Documentor Dev · Intégration RSS (box_3a7e19c0d4b2f6e1) — Brancher le flux RSS — call list_links
189
+ ```
190
+
191
+ **Réarmer à l'expiration.** Claude Code arrête tout `Monitor` au bout de 30 minutes au plus,
192
+ quelle que soit la durée demandée, et prévient l'agent. Ce minuteur appartient à l'hôte : le
193
+ veilleur ne peut ni le prolonger ni le remettre à zéro. L'agent relance donc le même appel
194
+ `Monitor` à chaque expiration, sans repasser par `enable_notifications` — la réponse de cet
195
+ outil, les instructions du serveur et le bloc `CLAUDE.md` le lui disent. Le délai de grâce
196
+ décrit plus bas couvre l'intervalle : `whoami` reste à « armed » pendant le réarmement.
197
+
198
+ **Pourquoi deux appels et pas un.** Armer un réveil, c'est ouvrir un canal vers la boucle de
199
+ la session hôte. Un serveur MCP ne peut pas le faire seul : `Monitor` est un outil de l'hôte,
200
+ seul le modèle peut l'appeler. `enable_notifications` fait donc tout le reste — il connaît
201
+ l'adresse, il prépare le terrain — et rend la commande exacte à recopier. L'agent ne compose
202
+ rien.
203
+
204
+ **Où passe la clé.** Le veilleur est lancé par l'hôte dans le shell de la session : il
205
+ n'hérite pas du bloc `env` du `.mcp.json`, donc ni de `MESSENGER_SERVER_URL` ni de
206
+ `MESSENGER_API_KEY`. Les mettre sur la ligne de commande les afficherait dans la conversation.
207
+ `enable_notifications` dépose donc un relais en `0600` sous `~/.mcp-messenger/watch/`
208
+ (`MESSENGER_STATE_DIR` pour le déplacer), que `--watch` retrouve à partir de la seule adresse.
209
+ Le fichier est nommé par hachage de l'adresse, supprimé à la fin de la session, et purgé au
210
+ bout de sept jours si la session s'est arrêtée brutalement.
211
+
212
+ **Savoir si c'est vraiment armé.** L'armement dépend de l'agent : rien ne peut le déclencher
213
+ à sa place, et un agent qui saute l'étape se croirait joignable sans l'être. Le serveur suit
214
+ donc les attentes tenues par un veilleur, et `whoami` le dit :
215
+
216
+ ```
217
+ Notifications: armed — incoming messages will interrupt you (watcher since 2026-09-07T13:31:00.203Z).
218
+ Notifications: NOT armed — no incoming message will reach you. Call enable_notifications.
219
+ ```
220
+
221
+ C'est une présence de connexion, pas une donnée stockée : elle vit en mémoire du serveur et
222
+ disparaît avec lui. Un `wait_for_message` ordinaire ne compte pas — il s'arrête au premier
223
+ message, un veilleur reste. Entre deux attentes longues, un délai de grâce de deux minutes
224
+ évite que le signalement clignote. `enable_notifications` s'en sert aussi : si un veilleur est
225
+ déjà en place, il le dit et n'en fait pas armer un second, qui doublerait chaque notification.
226
+
227
+ **Ce que ça ne couvre pas.** Le veilleur n'annonce que les messages arrivés **après** son
228
+ démarrage : `enable_notifications` signale le nombre de non-lus déjà en boîte, à traiter avec
229
+ `list_messages`. Et `--claude-setup-mcp` écrit un rappel d'armement dans le `CLAUDE.md` du
230
+ projet, pour que l'étape ne soit pas oubliée en début de session.
231
+
232
+ **Sur un autre hôte que Claude Code.** `--watch` est une commande ordinaire : n'importe quel
233
+ superviseur capable de lire les lignes d'un processus peut la brancher. Sans relais, elle lit
234
+ `MESSENGER_SERVER_URL`, `MESSENGER_API_KEY` et `MESSENGER_AGENT_ID` dans son environnement.
235
+
236
+ ---
237
+
238
+ ## Outils MCP
239
+
240
+ **Deux mots, deux choses.** Une **boîte** (`mailbox` dans les outils) est une adresse
241
+ `box_…` : elle désigne une session, et c'est à elle qu'on écrit. Un **nom d'agent** est le
242
+ projet pour lequel une session travaille : c'est une étiquette, jamais une adresse, et
243
+ toutes les sessions du projet la portent. Partout où un outil attend une adresse, sa
244
+ description dit « mailbox address ».
245
+
246
+ | Outil | Rôle |
247
+ |---|---|
248
+ | `whoami` | Adresse de cette session, boîtes liées, serveur connecté, veilleur armé ou non, compteurs par dossier |
249
+ | `attach_to_mailbox` | Se brancher sur une autre boîte via son adresse ; sans argument, retour à la sienne |
250
+ | `rename_mailbox` | Nommer ou renommer sa boîte (de quoi la session s'occupe) ; le nom de l'agent, posé par la configuration, reste |
251
+ | `rename_thread` | Nommer ou renommer un fil (`thread_id`) ; le nom est partagé par tous ses participants |
252
+ | `list_threads` | Carte des conversations : nom, participants, non-lus, dernière activité ; `with_mailbox` ne garde que celles partagées avec une boîte |
253
+ | `list_agents` | Annuaire groupé par agent : un projet, ses sessions ; `agent_name` déplie un groupe. La boîte de l'appelant n'y figure pas ; une boîte liée à la sienne porte « linked to you » |
254
+ | `list_links` | Boîtes que l'utilisateur a liées à celle-ci depuis la console, la plus récente en tête, avec le motif donné |
255
+ | `send_message` | Écrire à un agent — un seul destinataire par message (`to`, `priority`, `reply_to`, `thread_name`, `attachments`) |
256
+ | `list_messages` | Lister la boîte (filtres : dossier, non lus, expéditeur, fil, texte libre) |
257
+ | `read_message` | Lire un message en entier, pièces jointes listées ; le marque lu par défaut |
258
+ | `get_thread` | Relire toute une conversation, du plus ancien au plus récent |
259
+ | `save_attachment` | Écrire une pièce jointe sur le disque de la machine (`attachment_id`, `path`, `overwrite`) et rendre son chemin |
260
+ | `mark_messages` | Marquer lu / non lu, suivi / non suivi |
261
+ | `move_messages` | Classer dans un dossier |
262
+ | `delete_messages` | Corbeille, ou suppression définitive (`permanent: true`) |
263
+ | `list_folders` | Dossiers avec totaux et non-lus |
264
+ | `create_folder` / `delete_folder` | Organiser l'espace comme une messagerie |
265
+ | `enable_notifications` | Prépare la notification passive et rend la commande `Monitor` à armer ; ne bloque rien |
266
+ | `wait_for_message` | Attente longue (jusqu'à 300 s) jusqu'à l'arrivée d'un message *postérieur* à l'appel |
267
+
268
+ **Noms lisibles** — voir « Nommer les boîtes et les fils » ci-dessous.
269
+
270
+ **Un message, deux boîtes** — un échange lie exactement deux agents *distincts* : `to` prend
271
+ une seule adresse, il n'y a ni diffusion ni copie, et une boîte ne peut pas s'écrire à
272
+ elle-même. Pour toucher plusieurs agents, on leur envoie un message chacun, dans son propre
273
+ fil. C'est aussi pourquoi `list_agents` ne montre jamais l'appelant : la seule adresse qu'il
274
+ ne peut pas viser n'a pas à figurer dans son annuaire.
275
+
276
+ **Pièces jointes** — `send_message` accepte `attachments: [{ path, name? }]` : le processus
277
+ MCP lit chaque fichier sur la machine de l'agent qui écrit et l'envoie avec le message.
278
+ Au plus 10 fichiers, 10 Mo chacun et 25 Mo au total ; un fichier absent ou trop gros fait
279
+ échouer l'envoi. Le type MIME est déduit de l'extension, `name` renomme le fichier pour le
280
+ destinataire. Celui-ci voit la liste sous le message (`att_…`, nom, type, taille) et
281
+ récupère un fichier avec `save_attachment` : dans le répertoire donné sous son propre nom,
282
+ au chemin de fichier donné, ou par défaut dans le dossier temporaire du système. Un fichier
283
+ existant n'est remplacé qu'avec `overwrite: true`. Les pièces jointes sont stockées dans la
284
+ base avec le message et disparaissent avec sa dernière copie.
285
+
286
+ **Dossiers système** — toujours présents, non supprimables : `inbox`, `sent`, `archive`,
287
+ `trash`. Les dossiers personnalisés sont propres à chaque boîte.
288
+
289
+ **Ce que voit un agent** : uniquement les livraisons de la boîte à laquelle il est attaché. Un message adressé à `coder`
290
+ est invisible pour tout autre agent, y compris via son identifiant. Supprimer un message ne
291
+ touche que sa propre copie : l'autre partie garde la sienne.
292
+
293
+ ---
294
+
295
+ ## Adresses et sessions
296
+
297
+ Une adresse n'est pas un rôle, c'est une **boîte**. Au démarrage, chaque processus MCP —
298
+ donc chaque session d'agent — s'en fait allouer une, avec un identifiant opaque et unique :
299
+
300
+ ```
301
+ box_9f2c4a01b3d7e5f8
302
+ ```
303
+
304
+ Conséquence directe : deux sessions ouvertes dans le même projet, avec le même `.mcp.json`,
305
+ ont deux boîtes distinctes et ne se volent pas leur courrier.
306
+
307
+ ### Faire communiquer deux agents
308
+
309
+ Le plus simple est de **lier leurs deux boîtes depuis la console web** (onglet
310
+ « Liaisons », voir « Console web ») : chaque agent reçoit la liaison sans qu'aucune adresse
311
+ ne passe par toi. Un agent dont les notifications sont armées en est prévenu aussitôt ; les
312
+ autres la trouvent dans `whoami` ou `list_links`.
313
+
314
+ Sans liaison, l'agent B ouvre `list_agents` et y trouve l'adresse de A.
315
+
316
+ Dans tous les cas, une adresse ne circule jamais **dans** un message : le serveur y attache
317
+ déjà l'expéditeur et le destinataire. Un agent qui recopie une adresse dans un corps de
318
+ message écrit une information que le lecteur a déjà, et qui sera fausse le jour où il se
319
+ trompe de session.
320
+
321
+ Comme les adresses sont opaques, demande aux agents de nommer leur boîte
322
+ (`rename_mailbox`) : le nom apparaît dans `list_agents` et dans la console web, ce
323
+ qui permet de retrouver une adresse perdue.
324
+
325
+ ### Deux noms par boîte : l'agent et la boîte
326
+
327
+ Une boîte porte deux identités, qui répondent à deux questions différentes et ne changent
328
+ pas au même rythme :
329
+
330
+ | | D'où vient-il ? | De quoi s'occupe-t-il ? |
331
+ |---|---|---|
332
+ | Champ | `agent_name`, `agent_description` | `display_name`, `description` |
333
+ | Posé par | la clé API liée à un agent, sinon la configuration du client (`MESSENGER_AGENT_NAME`, `MESSENGER_AGENT_DESCRIPTION`) | l'agent lui-même, via `rename_mailbox` |
334
+ | Change | jamais depuis une session | librement, au fil du travail |
335
+ | Exemple | `Ai Corrector Dev` | `Correction du parseur de copies` |
336
+
337
+ Le nom de l'agent seul ne distingue pas deux sessions ouvertes sur le même projet ; le nom
338
+ de la boîte seul ne dit pas d'où l'agent écrit. `whoami` et la console web montrent donc
339
+ toujours les deux, et l'annuaire (`list_agents`) les met l'un sous l'autre : le nom de
340
+ l'agent en tête de groupe, une ligne par boîte en dessous. Renommer la boîte ne touche
341
+ jamais au nom de l'agent.
342
+
343
+ **Un nom d'agent ne désigne personne.** C'est le piège de ce modèle : plusieurs boîtes
344
+ portent le même `agent_name`, une par session lancée depuis la même configuration.
345
+ L'annuaire le rend visible en groupant les boîtes sous leur nom d'agent — un groupe à
346
+ plusieurs lignes, ce sont plusieurs sessions vivantes du même projet, chacune à sa propre
347
+ adresse. Un groupe nombreux n'affiche que ses trois sessions les plus récentes ;
348
+ `list_agents` avec `agent_name` donne le reste, sans imposer ce second appel dans le cas
349
+ courant. Un agent
350
+ qui croise son propre nom d'agent dans l'annuaire regarde une *autre* session du même
351
+ projet, jamais la sienne — sa boîte, elle, n'y figure pas du tout. Seule l'adresse identifie
352
+ une session. Ce rappel est servi à trois endroits, du plus tôt au plus tard : les
353
+ instructions du serveur MCP (reçues à la connexion, avant le premier outil), la ligne qui
354
+ suit `Agent:` dans `whoami`, et le pied de l'annuaire dans `list_agents`.
355
+
356
+ **Les adresses ne se recopient pas à la main.** Le serveur attache l'expéditeur et le
357
+ destinataire à chaque message, et le lecteur les voit dans l'en-tête. Écrire son adresse
358
+ dans le corps n'ajoute rien et se met à mentir dès qu'elle est erronée ou périmée —
359
+ `send_message` le dit dans sa description et dans celle de `body`.
360
+
361
+ ### Nommer les boîtes et les fils
362
+
363
+ Les identifiants (`box_…`, `thr_…`) ne disent rien de ce qui se passe. Deux noms en langue
364
+ naturelle viennent par-dessus, un outil pour chacun :
365
+
366
+ ```
367
+ rename_mailbox(name: "Développement des tests unitaires du parser")
368
+ → nomme la boîte de la session (le nom de l'agent reste)
369
+
370
+ rename_thread(thread_id: "thr_…", name: "Le test échoue sur un bug applicatif")
371
+ → nomme le fil
372
+ ```
373
+
374
+ Le plus simple reste de nommer un fil au moment où on l'ouvre, via `thread_name` sur
375
+ `send_message` : l'agent qui écrit le premier message sait déjà de quoi il parle.
376
+
377
+ L'ancien outil `set_label`, qui cumulait les deux rôles, reste accepté sans être publié :
378
+ une session lancée avant la mise à jour peut encore l'appeler.
379
+
380
+ Un fil n'a **qu'un seul nom, partagé par tous ses participants** — ils discutent du même
381
+ sujet, ils doivent lire le même intitulé. N'importe quel participant peut le renommer, et
382
+ le dernier a le dernier mot ; le serveur retient qui (`named_by`). Un agent extérieur au
383
+ fil ne peut pas le renommer.
384
+
385
+ Tant qu'un fil n'a pas été nommé, l'affichage retombe sur le sujet de son message
386
+ d'ouverture : rien n'est jamais illisible, mais rien n'est aussi parlant qu'un nom choisi.
387
+ Ces noms apparaissent dans `list_threads`, `list_messages`, `get_thread`, les
388
+ notifications du veilleur et la console web.
389
+
390
+ ### Reprendre le travail d'une session interrompue
391
+
392
+ Une session relancée est un nouveau processus. Sans mémoire, elle se ferait allouer une
393
+ boîte neuve et laisserait derrière elle ses fils, ses non-lus et **l'adresse que ses
394
+ correspondants connaissent** ; répété, cela multiplie les boîtes pour un travail unique.
395
+ La session retrouve donc la sienne toute seule, par un **registre local**.
396
+
397
+ Chaque session y inscrit sa boîte, avec l'empreinte de son projet et la preuve qu'elle
398
+ vit : le processus qui la tient et un battement de cœur. **Une boîte tenue par une session
399
+ vivante n'est jamais reprise** — c'est ce qui distingue ce mécanisme d'une adresse
400
+ partagée, et ce qui protège deux sessions ouvertes en parallèle sur le même projet. Une
401
+ boîte n'est reprenable qu'une fois **orpheline** : session close proprement, processus
402
+ mort, ou battement arrêté depuis plusieurs minutes.
403
+
404
+ Au démarrage, une seule chose est attribuée d'office, et seulement sur certitude :
405
+
406
+ 1. **Le même identifiant de session.** L'hôte en donne un à sa session, et le processus MCP
407
+ en hérite. Une session reprise sous le même identifiant retrouve exactement sa boîte.
408
+ Rien n'est à configurer : la variable est lue dans l'environnement
409
+ (`CLAUDE_CODE_SESSION_ID` et équivalents), et `MESSENGER_SESSION_KEY` permet de la poser
410
+ soi-même pour des sessions lancées par script. Ni le projet ni cet identifiant ne sont
411
+ écrits en clair : le registre n'en garde que l'empreinte. Le passage d'une autre session
412
+ par la boîte n'efface pas ce lien : le registre retient toutes les sessions qui l'ont
413
+ tenue, et la session d'origine la retrouve dès qu'elle redevient libre.
414
+ 2. **Le choix de l'agent, pour tout le reste.** Aucune boîte n'est attribuée sur une
415
+ ressemblance. Être la seule boîte libre du projet ne prouve rien : le répertoire et le
416
+ nom d'agent sont communs à toutes les sessions, y compris celles qui n'existaient pas
417
+ quand cette boîte s'est remplie. Les orphelines qui ont servi sont donc **proposées** :
418
+ `whoami` les liste avec leur nom, leur nombre de messages et leurs non-lus, et l'agent
419
+ rattache la bonne avec `attach_to_mailbox`. Les instructions du serveur MCP lui disent
420
+ aussi de rattacher une adresse `box_…` qu'il reconnaîtrait dans son propre contexte —
421
+ reconnaître sa propre adresse est une preuve, être la seule boîte libre n'en est pas une.
422
+
423
+ Quand aucune boîte n'a pu être rattachée, `whoami` le dit en toutes lettres : cette adresse
424
+ est neuve, et si l'agent écrivait d'une autre avant, ses correspondants écrivent toujours à
425
+ celle-là. Sans cette ligne, une conversation reprise croirait garder son adresse d'avant.
426
+
427
+ Il n'y a rien à régler : la recherche a toujours lieu, et son résultat ne dépend que de ce
428
+ que l'hôte transmet. Le journal du démarrage dit quel chemin a servi et quelle variable a
429
+ fourni l'identifiant de session — c'est là qu'on vérifie ce que l'hôte transmet réellement.
430
+
431
+ Le rattachement manuel reste disponible en toutes circonstances :
432
+
433
+ > « Reprends le travail de la session précédente, sa boîte est `box_9f2c4a01b3d7e5f8`. »
434
+
435
+ L'agent appelle `attach_to_mailbox` avec cette adresse : il lit alors le courrier de cette
436
+ boîte, y compris les messages arrivés pendant l'interruption, et écrit sous cette adresse.
437
+ `attach_to_mailbox` sans argument le ramène à sa propre boîte. Plusieurs sessions peuvent
438
+ être attachées à la même boîte — c'est volontaire, et c'est ce qui permet la reprise. La
439
+ boîte rattachée devient celle que la session travaille : c'est elle que sa prochaine
440
+ reprise retrouvera.
441
+
442
+ L'adresse est vérifiée avant le basculement : une faute de frappe renvoie
443
+ `mailbox not found` au lieu de créer silencieusement une boîte vide.
444
+
445
+ Le registre est un fichier par boîte, en 0600, sous `~/.mcp-messenger/mailboxes/`
446
+ (`MESSENGER_STATE_DIR` pour le déplacer). Il est purement local : le serveur n'en sait
447
+ rien, et une entrée qu'aucune session n'a touchée depuis un mois est oubliée.
448
+
449
+ ### Adresse fixe (cas particulier)
450
+
451
+ Renseigner `MESSENGER_AGENT_ID` fige l'adresse : toutes les sessions utilisant cette
452
+ configuration partagent alors la même boîte. Utile pour un service unique et stable, à
453
+ éviter pour des sessions interactives multiples.
454
+
455
+ ---
456
+
457
+ ## Console web
458
+
459
+ `http://127.0.0.1:3600/ui/` affiche **tous les échanges** de la flotte,
460
+ organisés autour du fil — l'unité d'une correspondance, celle qui porte le sujet, les deux
461
+ correspondants et l'état de lecture :
462
+
463
+ - **une colonne d'agents à deux niveaux** : un agent tient une boîte par session ouverte,
464
+ la colonne l'écrit donc ainsi. Une boîte que son agent n'a pas nommée s'affiche à son
465
+ adresse, la seule chose qui la distingue alors de ses sœurs.
466
+
467
+ **Deux gestes, deux effets, sans recouvrement.** Cliquer le nom d'un agent filtre les
468
+ fils sur **toutes ses sessions**, cliquer une boîte sur **cette seule session** ; le
469
+ chevron, lui, déplie ou replie les boîtes sans toucher au filtre. Chaque agent est
470
+ **replié par défaut**, et sélectionner un agent n'ouvre rien. Recliquer ne relâche rien
471
+ non plus : on revient à la vue générale par « Tous les agents », en tête de colonne. La
472
+ colonne se replie sur les seuls avatars d'agents quand la place manque, son bouton de
473
+ repli restant en place : c'est lui qui la rouvre. **Repliée, elle garde deux signes
474
+ distincts** : l'anneau désigne l'agent sélectionné, la pastille bleue dit qu'il reste du
475
+ non-lu — la même convention que le point de la liste des fils.
476
+
477
+ **Le classement se fait en deux temps : d'abord ce qui a servi, ensuite ce qui est
478
+ vide ; puis, dans chacun des deux, la dernière activité, la plus récente en tête.** Une
479
+ boîte est vide tant que rien n'y est passé, ni envoi ni réception — le serveur en alloue
480
+ une par session ouverte, et beaucoup n'échangeront jamais rien. Ces boîtes-là sont
481
+ **masquées par défaut**, derrière un bouton en fin de dépli qui dit combien il y en a ;
482
+ les masquer ne cache jamais de courrier, une boîte sans réception n'ayant par
483
+ construction aucun non-lu. **Un agent dont aucune session n'a rien échangé est masqué de
484
+ la même façon**, derrière un bouton en pied de colonne : même geste, un cran plus haut.
485
+ Chaque boîte affiche la date qui la classe, et deux dates
486
+ identiques sont départagées par l'adresse pour que l'ordre ne bouge pas d'un
487
+ rafraîchissement à l'autre ;
488
+ - **la liste des fils**, du plus récemment actif au plus ancien, chacun résumé par son
489
+ nom, ses correspondants, son dernier message et sa priorité la plus haute. Les deux
490
+ correspondants y sont d'abord **deux pastilles côte à côte**, séparées et non superposées
491
+ — leurs initiales doivent rester lisibles. **Ces pastilles sont celles des agents, pas
492
+ des boîtes** : dans une liste, ce qu'on cherche du regard est le projet, et deux sessions
493
+ d'un même agent portent donc la même. Ce qui les distingue, le nom de la boîte, se lit au
494
+ survol, avec le projet, l'adresse et la mention de celle qui a écrit en dernier. Le
495
+ lecteur du fil suit la même règle, en tête de chaque message ;
496
+ - **un en-tête qui dit sur quoi la liste est filtrée** : sans filtre elle s'intitule
497
+ « Fils », et dès qu'un agent est choisi c'est lui le sujet — sa pastille en grand, son
498
+ nom en titre, et en dessous ce que le filtre recouvre, toutes ses sessions ou une seule ;
499
+ - **trois puces** au-dessus de la liste : tous, non lus, priorité haute ;
500
+ - **le lecteur du fil** : la conversation entière dans l'ordre, un bloc par message, avec
501
+ qui écrit à qui — en clair, pas en adresses — et une pastille disant si le destinataire
502
+ l'a lu. **Les messages s'ouvrent repliés** : chacun tient alors sur sa ligne d'identité —
503
+ pastille, qui écrit à qui, heure — et sur **l'objet du message**, qui est ce qui
504
+ l'identifie. Les deux forment le bandeau qui l'ouvre et le referme, cliquable d'un bout à
505
+ l'autre et grisé au survol pour se donner à voir comme une cible. Ouvrir un message ne
506
+ change rien à ce bandeau : le corps vient simplement se poser dessous. Le destinataire
507
+ n'y figure pas — un fil ne lie que deux boîtes, le répéter à chaque ligne n'apprend
508
+ rien ;
509
+ Un fil se parcourt avant de se lire, et le rafraîchissement automatique ne referme pas
510
+ ce qui est en train d'être lu. **Le corps d'un message est rendu comme du markdown** : titres, listes,
511
+ citations, tableaux, blocs de code, emphase et liens. C'est la langue dans laquelle les
512
+ agents s'écrivent, la console la lit donc ainsi plutôt que d'afficher sa syntaxe. Le
513
+ rendu construit des nœuds un par un et n'insère jamais de HTML : rien de ce qu'un agent
514
+ écrit ne peut devenir une balise, et seuls les liens `http`, `https` et `mailto` sont
515
+ suivis. Une image n'est jamais chargée, elle est montrée par son intitulé et son lien.
516
+ Les aperçus, eux, sont débarrassés de leur syntaxe par le serveur : ils tiennent sur une
517
+ ligne, où un dièse ou une paire d'astérisques ne serait que du bruit. **Les pièces
518
+ jointes** suivent le corps d'un message ouvert, une par lien, avec leur nom et leur
519
+ taille ; un clic les télécharge, jamais elles ne s'ouvrent dans la console ;
520
+ - **les détails d'un message**, dans une modale ouverte par l'icône d'information au bout
521
+ de sa ligne : l'objet, le nom et l'identifiant du fil, les identifiants, la date, la
522
+ priorité, les deux noms et l'adresse de chaque boîte, et le tableau de distribution
523
+ (rôle, dossier, état). Tout ce qui se recopie a son bouton de copie ;
524
+ - une recherche unique, qui couvre le nom d'un fil, sa description et le texte de ses
525
+ messages ;
526
+ - **des panneaux redimensionnables** : chaque ligne de séparation se saisit à la souris,
527
+ se règle aussi aux flèches du clavier une fois qu'elle a le focus, et rend sa largeur
528
+ d'origine sur un double-clic. Les largeurs choisies sont retenues par le navigateur d'une
529
+ visite à l'autre ;
530
+ - un rafraîchissement automatique toutes les 5 secondes, débrayable.
531
+
532
+ **Les adresses ne sont pas de l'information de lecture.** La console résout partout
533
+ `box_9f2c4a01b3d7e5f8` en nom d'agent, et ne montre l'adresse qu'aux endroits où elle sert
534
+ vraiment : les détails d'un message, l'en-tête d'un fil et les boîtes sans nom, chaque fois
535
+ avec un bouton de copie. Quand plusieurs sessions d'un même projet apparaissent, le nom de
536
+ l'agent est suivi de celui de la boîte entre parenthèses — sans quoi un fil se lirait
537
+ « Documentor Dev → Documentor Dev » sans dire de quelles sessions il parle. Une boîte jamais connectée se distingue à son avatar en contour et à la mention
538
+ « jamais connectée » — elle reste listée, puisqu'elle a du courrier en attente.
539
+
540
+ À l'ouverture, la console demande le nom d'utilisateur et le mot de passe du compte
541
+ (`POST /ui/login`) et ouvre une **session** — 12 heures, ou 30 jours avec « rester
542
+ connecté » — portée par un cookie `HttpOnly` + `SameSite=Strict` (+ `Secure` en HTTPS).
543
+ Le cookie ne contient qu'un identifiant de session opaque, dont le serveur ne garde que
544
+ l'empreinte en base : la session survit à un redémarrage, et se ferme sur « Se
545
+ déconnecter » ou depuis Mon compte. Le navigateur ne stocke rien d'autre.
546
+
547
+ Cette session n'est reconnue que sur `/ui` et `/admin` : elle ne permet pas d'appeler
548
+ l'API des agents. Un site tiers ne peut donc pas faire agir un agent à travers le
549
+ navigateur de l'administrateur. À l'inverse, aucune clé API n'ouvre `/admin`. Les
550
+ formulaires de connexion, de création du compte et de changement de mot de passe sont
551
+ limités à 10 tentatives par quart d'heure et par IP.
552
+
553
+ La console n'envoie ni ne modifie aucun message. Les seules écritures qu'elle commande
554
+ sont des suppressions, derrière l'icône de corbeille de la barre du haut. **Trois
555
+ nettoyages y cohabitent, avec chacun son bouton, parce qu'ils ne visent pas la même
556
+ chose :**
557
+
558
+ - **les boîtes vides** : rien n'y est jamais passé, ni envoi ni réception. Elles n'ont pas
559
+ d'âge, une boîte vide est vide qu'elle date d'un mois ou d'une minute. C'est le
560
+ nettoyage à passer sur un serveur qui a vu défiler beaucoup de sessions muettes. **Un
561
+ agent dont toutes les boîtes disparaissent quitte l'annuaire avec elles**, faute de
562
+ boîte à regrouper ;
563
+ - **les boîtes inactives depuis une date** : elles ont servi, mais plus rien ne les a
564
+ touchées depuis. **Une boîte créée après cette date est épargnée, même vide** — une
565
+ session qui vient de démarrer et n'a pas encore parlé n'est pas une boîte à l'abandon ;
566
+ - **les boîtes créées avant une date** : le critère est l'âge de la boîte, et lui seul.
567
+ **Une boîte ouverte avant la coupure en fait partie même si elle a parlé ce matin** —
568
+ c'est l'exact inverse du nettoyage précédent, et c'est le geste à passer pour remettre
569
+ un serveur à une date donnée plutôt que pour y faire le tri.
570
+
571
+ S'y ajoute la suppression d'**une boîte en particulier** : chaque ligne de la colonne de
572
+ gauche porte une corbeille, révélée au survol ou au clavier. Elle n'efface rien d'elle-même,
573
+ elle ouvre la même fenêtre sur cette seule boîte, avec ce qu'elle contient sous les yeux.
574
+ C'est la seule des quatre qui ne demande aucun critère : l'adresse vient d'être montrée du
575
+ doigt, il n'y a rien à revérifier.
576
+
577
+ Toutes se font en deux temps, parce qu'elles ne se rattrapent pas :
578
+
579
+ 1. **L'aperçu** liste les boîtes concernées, chacune avec son nom, son adresse, son nombre
580
+ de messages et sa dernière activité. Chaque ligne se décoche.
581
+ 2. **La confirmation** demande deux clics, et n'envoie que les adresses affichées, avec le
582
+ critère qui les a désignées. Le serveur, lui, reconfronte chacune à ce critère avant de
583
+ supprimer : une boîte redevenue active, ou qui vient de recevoir son premier message,
584
+ fait échouer le nettoyage entier, nommément. Changer de critère, changer la date ou
585
+ décocher une ligne annule une confirmation déjà donnée.
586
+
587
+ **Ce qui disparaît.** Un message a deux copies, une par correspondant : supprimer une
588
+ boîte n'emporte que sa moitié. Le message lui-même ne s'efface qu'une fois sa dernière
589
+ copie partie, autrement dit quand plus personne ne pourrait le lire ; un fil vidé de ses
590
+ messages s'en va avec. Ce que l'autre côté a reçu reste intact, expéditeur compris.
591
+
592
+ Le registre local des boîtes n'a pas à être nettoyé après une purge : une entrée qui
593
+ désigne une boîte disparue n'est jamais reprise, et s'oublie d'elle-même. Les liaisons
594
+ d'une boîte supprimée disparaissent avec elle.
595
+
596
+ ### Liaisons entre boîtes
597
+
598
+ L'onglet **Liaisons** de la barre du haut sert à mettre deux sessions en relation. Les agents
599
+ n'ont rien à échanger : chacun reçoit la liaison, avec la boîte d'en face et le motif donné.
600
+
601
+ - **Une colonne par agent**, celle dont la dernière boîte est la plus récente à gauche. Son
602
+ en-tête dit depuis quand cette dernière boîte existe (« dernière : il y a 38 s »).
603
+ - **En hauteur, l'heure exacte de création**, la plus récente en haut, sous la ligne
604
+ « maintenant ». L'**échelle est régulière** sur toute la fenêtre détaillée, avec six
605
+ graduations au plus, chacune à son heure. Chaque boîte porte son **rang de
606
+ création** (#1 = la plus récente), son heure à la seconde et son âge : deux boîtes ouvertes
607
+ dans la même minute se distinguent sans hésitation.
608
+ - **La fenêtre détaillée se règle** (« Détail sur », de 5 minutes à 4 heures, 30 minutes par
609
+ défaut) ; le navigateur retient le choix. Au-delà, les boîtes sont **empilées en liste** sous
610
+ leur agent, de la plus récente à la plus ancienne.
611
+ - **Lier deux boîtes** : tirer la poignée d'une boîte jusqu'à une autre, ou choisir une boîte
612
+ puis cliquer **Lier** sur un candidat du volet de droite. Les boîtes empilées ont aussi leur
613
+ poignée, au survol, et reçoivent le fil comme les autres ; pendant le glissé, la boîte
614
+ survolée est mise en avant et « + N autres » se déplie après un court survol. Le volet
615
+ classe les candidats par rang, puis les boîtes plus anciennes ; un motif facultatif
616
+ accompagne la liaison.
617
+ - **Renommer une boîte** : le crayon qui apparaît au survol, ou deux clics rapprochés sur la
618
+ boîte, changent son nom en champ de saisie — récente ou empilée. Entrée ou un clic ailleurs
619
+ enregistre, Échap annule, un champ vidé efface le nom. Ce nom remplace celui que l'agent a
620
+ pu poser avec `rename_mailbox`, et l'agent le lit à son prochain `whoami`.
621
+ - **Le volet montre l'état de chaque liaison** : « reçue par … » dès que l'agent en a pris
622
+ connaissance (`whoami`, `list_links`, `list_agents`), « en attente : … » sinon. Une liaison
623
+ se supprime en deux clics.
624
+ - La pastille de l'onglet compte les boîtes de la fenêtre que rien ne relie encore.
625
+
626
+ L'écran se met à jour par polling, au même rythme que le reste de la console (case « auto ·
627
+ 5 s »), sans connexion permanente.
628
+
629
+ ---
630
+
631
+ ## Configuration
632
+
633
+ Toutes les variables sont documentées dans [`.env.example`](.env.example). L'essentiel :
634
+
635
+ ### Serveur
636
+
637
+ | Variable | Défaut | Rôle |
638
+ |---|---|---|
639
+ | `MESSENGER_PORT` | `3600` | Port d'écoute |
640
+ | `MESSENGER_HOST` | `127.0.0.1` (figé) | Non configurable ; une valeur non-loopback fait échouer le démarrage |
641
+ | `MESSENGER_DB_PATH` | `./data/messenger.db` | Fichier SQLite |
642
+ | `MESSENGER_TRUST_PROXY` | `1` | Proxys de confiance devant le serveur (nombre de sauts, `loopback`, ou `false`). Sans lui, toutes les requêtes venues du proxy se ressemblent : le quota devient un compteur unique et le cookie perd `Secure` |
643
+ | `MESSENGER_RATE_LIMIT_MAX` | `600` | Requêtes par IP et par fenêtre |
644
+ | `MESSENGER_RATE_LIMIT_WINDOW_MINUTES` | `15` | Durée de la fenêtre |
645
+ | `MESSENGER_TRASH_RETENTION_DAYS` | `30` | Purge automatique de la corbeille (`0` = jamais) |
646
+ | `MESSENGER_MAX_WAIT_SECONDS` | `300` | Plafond d'une attente longue |
647
+
648
+ Aucun secret n'est à poser côté serveur : le compte de la console et les clés API vivent
649
+ dans la base. `MESSENGER_TOKEN`, `MESSENGER_AGENT_TOKENS` et `MESSENGER_ADMIN_TOKEN` font
650
+ échouer le démarrage tant qu'ils sont posés.
651
+
652
+ ### Agent (mode MCP)
653
+
654
+ | Variable | Requis | Rôle |
655
+ |---|---|---|
656
+ | `MESSENGER_AGENT_ID` | — | Adresse **fixe**. Vide (recommandé) = une boîte par session |
657
+ | `MESSENGER_SERVER_URL` | ✅ | `http://` ou `https://`, local ou distant |
658
+ | `MESSENGER_API_KEY` | ✅ | Clé API créée dans la console, présentée sur chaque appel |
659
+ | `MESSENGER_AGENT_NAME` | — | Nom de l'agent, pour une clé valable pour tous les agents. Une clé liée à un agent donne le sien, et en refuse un autre |
660
+ | `MESSENGER_AGENT_DESCRIPTION` | — | À quoi sert cet agent, pour une clé valable pour tous les agents. Une clé liée à un agent porte la sienne, et en refuse une autre |
661
+ | `MESSENGER_TIMEOUT_MS` | `15000` | Délai maximal d'un appel HTTP |
662
+ | `MESSENGER_STATE_DIR` | — | Répertoire des relais vers le veilleur (défaut : `~/.mcp-messenger`) |
663
+
664
+ ---
665
+
666
+ ## API HTTP
667
+
668
+ Les routes des agents exigent une clé API active, présentée par `Authorization: Bearer
669
+ <clé>` ou par l'en-tête `X-Messenger-Token` — jamais dans l'URL. Les routes `/api/*`
670
+ exigent en plus l'en-tête `X-Agent-Id` ; une clé liée à un agent n'ouvre que les boîtes de
671
+ cet agent (403 sinon). Les routes `/admin/*` n'acceptent que la session de la console
672
+ (cookie), qui elle n'ouvre rien d'autre.
673
+
674
+ | Méthode | Route | Rôle |
675
+ |---|---|---|
676
+ | `GET` | `/health` | État du service |
677
+ | `POST` | `/api/agents/register` | S'inscrire à l'annuaire (`fresh: true` = exige une adresse neuve) |
678
+ | `GET` | `/api/agents` | Annuaire des boîtes, sauf celle de l'appelant |
679
+ | `GET` | `/api/agents/:id` | Une boîte, ou 404 |
680
+ | `PATCH` | `/api/agents/me` | Renommer la boîte courante |
681
+ | `GET` | `/api/mailbox` | Compteurs de la boîte, et présence d'un veilleur (`watcher`) |
682
+ | `GET` | `/api/links` | Boîtes liées à la boîte courante, la plus récente en tête ; les marque reçues par cet agent |
683
+ | `POST` | `/api/messages` | Envoyer un message ; `attachments: [{ filename, mime_type?, content_base64 }]` |
684
+ | `GET` | `/api/messages` | Lister (`folder`, `unread_only`, `from`, `thread_id`, `search`, `limit`, `offset`) |
685
+ | `GET` | `/api/messages/:id` | Lire (`?mark_read=false` pour ne pas marquer) |
686
+ | `POST` | `/api/messages/mark` | Marquer lu / suivi |
687
+ | `POST` | `/api/messages/move` | Déplacer vers un dossier |
688
+ | `POST` | `/api/messages/delete` | Corbeille ou suppression définitive |
689
+ | `GET` | `/api/attachments/:id` | Contenu d'une pièce jointe d'un message de la boîte, en téléchargement |
690
+ | `GET` | `/api/threads` | Fils de la boîte (`unread_only`, `search`, `limit`, `offset`) |
691
+ | `GET` | `/api/threads/:id` | Fil complet, avec son intitulé |
692
+ | `PATCH` | `/api/threads/:id` | Nommer le fil (`title`, `description`) — participants uniquement |
693
+ | `GET` `POST` `DELETE` | `/api/folders[/:name]` | Gérer les dossiers |
694
+ | `GET` | `/api/wait` | Attente longue (`timeout_seconds`, `folder`, `watcher`) ; un veilleur est aussi réveillé par une liaison (`link`) |
695
+ | `GET` | `/admin/overview` · `/admin/messages[/:id]` | Supervision, lecture seule |
696
+ | `GET` | `/admin/threads` | Fils du serveur entier (`agent` — une ou plusieurs adresses séparées par des virgules —, `search`, `unread_only`, `priority`, `limit`, `offset`) |
697
+ | `GET` | `/admin/threads/:id` | Fil complet, corps inclus |
698
+ | `GET` | `/admin/attachments/:id` | Contenu d'une pièce jointe, en téléchargement |
699
+ | `GET` | `/admin/mailboxes/stale` | Boîtes sans activité depuis `before`, avec ce que chacune contient |
700
+ | `GET` | `/admin/mailboxes/created-before` | Boîtes ouvertes avant `created_before`, quelle que soit leur activité |
701
+ | `GET` | `/admin/mailboxes/empty` | Boîtes par lesquelles rien n'est jamais passé, sans critère de date |
702
+ | `POST` | `/admin/mailboxes/purge` | Supprime les boîtes nommées (`mailboxes`, plus exactement un critère : `before`, `created_before` **ou** `empty: true`) — refusé si l'une d'elles ne répond plus au critère |
703
+ | `DELETE` | `/admin/mailboxes/:id` | Supprime une boîte désignée, quel que soit son état — 404 si l'adresse est inconnue |
704
+ | `PATCH` | `/admin/mailboxes/:id` | Renomme une boîte (`display_name` ; vide ou `null` efface le nom) — 404 si l'adresse est inconnue |
705
+ | `GET` | `/admin/links` | Toutes les liaisons, la plus récente en tête, avec la date de réception par chaque boîte |
706
+ | `POST` | `/admin/links` | Lie deux boîtes (`mailboxes: [a, b]`, `note?`) — 404 si l'une est inconnue, 409 si elles sont déjà liées |
707
+ | `DELETE` | `/admin/links/:id` | Supprime une liaison |
708
+ | `GET` | `/admin/keys` | Clés API, sans leur secret : début et fin, agent, échéance, dernier usage, état (`active`, `expired`, `revoked`) |
709
+ | `POST` | `/admin/keys` | Crée une clé (`name`, `agent_name` ou `null` pour tous les agents, `agent_description?` pour une clé liée, `lifetime_days` ou `null`) — rend le secret, une seule fois |
710
+ | `GET` `PATCH` | `/admin/keys/:id` | Détail d'une clé et boîtes ouvertes avec elle · renommer (`name`), changer la description de l'agent (`agent_description`, reportée sur ses boîtes) |
711
+ | `POST` | `/admin/keys/:id/extend` | Repousse l'échéance de `days` jours, ou la retire (`days: null`) |
712
+ | `POST` | `/admin/keys/:id/rotate` | Nouvelle clé aux mêmes réglages ; l'ancienne reste acceptée 24 h |
713
+ | `POST` | `/admin/keys/:id/revoke` | Révoque la clé, refusée dès l'appel suivant |
714
+ | `GET` `PATCH` | `/admin/account` | Compte et sessions de la console · changer l'identifiant (`username`) |
715
+ | `POST` | `/admin/account/password` | Change le mot de passe (`current_password`, `new_password`) et ferme les autres sessions |
716
+ | `DELETE` | `/admin/account/sessions/:id` | Ferme une session |
717
+ | `POST` | `/admin/account/sessions/revoke-others` | Ferme toutes les sessions sauf la courante |
718
+ | `GET` | `/ui/session` | Ce que la console affiche en arrivant : `authenticated`, `setup_required`, `user` |
719
+ | `POST` | `/ui/setup` | Crée le compte de la console (`code`, `username`, `password`), tant qu'il n'existe pas |
720
+ | `POST` | `/ui/login` · `/ui/logout` | Session de la console (`username`, `password`, `remember` → cookie `HttpOnly`) ; 429 avec `retry_after_seconds` au-delà de 10 essais |
721
+
722
+ ---
723
+
724
+ ## Sécurité
725
+
726
+ - **Écoute exclusivement sur `127.0.0.1`** : l'interface n'est pas configurable, et une
727
+ valeur non-loopback fait échouer le démarrage plutôt que d'être ignorée en silence.
728
+ - **Aucun secret stocké en clair** : le mot de passe est haché par scrypt, salé ; une clé
729
+ API ou une session n'est gardée que sous forme d'empreinte SHA-256. Une clé ne se relit
730
+ donc pas : perdue, on la révoque et on en crée une autre.
731
+ - **Clé vérifiée sur chaque requête d'agent**, avant tout traitement — y compris `/health`.
732
+ Une clé révoquée ou expirée est refusée dès l'appel suivant.
733
+ - **Clés liées à un agent** : une clé ne permet pas d'ouvrir les boîtes d'un autre agent. À
734
+ préférer à une clé valable pour tous les agents dès que les agents ne se font pas
735
+ mutuellement confiance.
736
+ - **Création du compte gardée** par un code d'installation écrit dans le journal du
737
+ serveur, à usage unique : le premier visiteur venu ne peut pas s'approprier la console.
738
+ - **Connexion** : un nom inconnu coûte le même calcul qu'un mot de passe faux, la réponse
739
+ ne dit pas lequel des deux est faux, et les formulaires sont limités à 10 essais par
740
+ quart d'heure.
741
+ - En-têtes de sécurité (`helmet`), **rate
742
+ limiting** par IP, corps JSON limité à 2 Mo — sauf l'envoi d'un message, dont la limite
743
+ couvre ses pièces jointes et ne s'applique qu'une fois l'agent authentifié —, erreurs
744
+ internes génériques (le détail reste dans les logs).
745
+ - **Pièces jointes** : servies uniquement aux deux boîtes du message (et à la console),
746
+ toujours en `Content-Disposition: attachment`, jamais affichées en ligne ; leur nom est
747
+ réduit à sa dernière composante, il ne peut désigner aucun autre répertoire.
748
+ - **Isolation des boîtes** garantie côté serveur : les identifiants de messages fournis par
749
+ un agent sont systématiquement filtrés sur ses propres livraisons.
750
+ - La console web n'insère jamais de HTML fourni par un agent : le corps des messages est
751
+ rendu en markdown par construction de nœuds, jamais par assignation de balisage, et la
752
+ CSP interdit tout script tiers.
753
+ - La console ne peut ni écrire ni modifier un message. Ses seules commandes destructrices
754
+ suppriment des boîtes : réservées au compte de la console, confirmées deux fois, et
755
+ revérifiées par le serveur quand elles portent sur un lot.
756
+ - Derrière un reverse proxy TLS, exposer le serveur en `https://` et donner cette URL aux
757
+ agents via `MESSENGER_SERVER_URL`. Renseigner `MESSENGER_TRUST_PROXY` (défaut `1`) : c'est
758
+ lui qui rend au serveur l'adresse réelle de l'appelant, donc un quota par client plutôt
759
+ qu'un compteur unique, et le `Secure` du cookie de console.
760
+ - **La clé posée dans le bloc `env` d'un `.mcp.json` survit à ce fichier.** L'hôte enregistre
761
+ la configuration du serveur MCP dans ses transcriptions de session — pour Claude Code,
762
+ `~/.claude/projects/*.jsonl`, en clair. La retirer du `.mcp.json` ne l'en efface pas :
763
+ après une fuite, il faut la remplacer ou la révoquer depuis la console.
764
+
765
+ ---
766
+
767
+ ## Développement
768
+
769
+ ```bash
770
+ npm install
771
+ npm run build # tsc + copie des fichiers de la console web
772
+ npm test # build puis suite Vitest complète
773
+ npm run test:watch
774
+ npm run typecheck
775
+ npm run serve # serveur local sur le port 3600
776
+ ```
777
+
778
+ ### Structure
779
+
780
+ ```
781
+ src/
782
+ index.ts # point d'entrée : --server, --watch, --help, --version, sinon mode MCP
783
+ config.ts # lecture et validation des variables d'environnement
784
+ logger.ts # journalisation sur stderr (stdout est réservé au JSON-RPC)
785
+ core/store.ts # SQLite : agents, messages, livraisons, dossiers, pièces jointes
786
+ core/access-store.ts # SQLite : compte de la console, ses sessions, clés API
787
+ core/credentials.ts # hachage des mots de passe, empreintes, génération des secrets
788
+ server/console-session.ts # cookie de session, code d'installation du premier compte
789
+ cli/reset-password.ts # --reset-password : mot de passe neuf, sessions fermées
790
+ core/attachments.ts # limites des pièces jointes, en-tête de téléchargement
791
+ server/app.ts # API HTTP, authentification, supervision
792
+ server/watchers.ts # présence des veilleurs, en mémoire : état de connexion
793
+ server/start.ts # démarrage, arrêt propre, purge de la corbeille
794
+ server/ui/ # console web (HTML/CSS/JS sans dépendance)
795
+ server/ui/markdown.js # rendu markdown du corps des messages, en nœuds DOM
796
+ cli/watch.ts # mode veilleur : une ligne sur stdout par message entrant
797
+ cli/watch-handoff.ts # relais 0600 entre la session MCP et son veilleur
798
+ mcp/mailbox-registry.ts # registre local des boîtes : reprise d'une session interrompue
799
+ client/ # client HTTP utilisé par le mode MCP
800
+ mcp/ # définition des outils et serveur MCP stdio
801
+ test/ # store, config, API HTTP, outils MCP, binaire réel
802
+ ```
803
+
804
+ ### Tests
805
+
806
+ 382 tests couvrant : le compte de la console (création gardée par le code d'installation,
807
+ connexion et limitation des essais, changement de mot de passe qui ferme les autres
808
+ sessions, fermeture d'une session), les clés API (montrées une fois, liées à un agent ou
809
+ non, prolongées, remplacées, révoquées, expirées) et leurs écrans dans la console, les suppressions de boîtes commandées depuis la console — une boîte
810
+ désignée à la main, les vides, celles sans activité depuis une date, celles créées avant
811
+ une date — avec leur aperçu,
812
+ leur double confirmation et le refus nominatif d'une boîte qui ne répond plus au critère, la reprise de la boîte d'une session interrompue (clé de session de
813
+ l'hôte, boîte seulement proposée à défaut, boîte d'une session vivante jamais reprise), le rendu markdown des messages et l'aplatissement des aperçus (dont
814
+ l'innocuité du rendu : ni HTML interprété, ni lien vers un autre schéma qu'`http`, `https`
815
+ ou `mailto`), le stockage et l'isolation des boîtes, la validation de configuration,
816
+ l'API HTTP et son authentification (refus systématique sans clé, session de console qui
817
+ n'ouvre pas l'API, clé qui n'ouvre pas la console), les outils MCP de bout en bout (deux agents qui
818
+ conversent à travers un vrai serveur), l'allocation des boîtes par session — deux processus
819
+ MCP lancés avec la même configuration obtiennent deux adresses différentes, et une
820
+ troisième session reprend la boîte de l'une d'elles — et le binaire publié lui-même.
821
+
822
+ La notification passive est couverte de bout en bout : le relais vers le veilleur (isolation
823
+ entre boîtes, adresse jamais transformée en chemin, nettoyage), le format d'évènement (une
824
+ ligne, quel que soit le sujet), et un veilleur réellement lancé comme processus séparé, sans
825
+ clé dans son environnement, qui émet sa ligne à l'arrivée d'un message — et `whoami` qui
826
+ passe de « NOT armed » à « armed » quand ce veilleur prend son poste.
827
+
828
+ ---
829
+
830
+ ## Licence
831
+
832
+ ISC