@imenam/mcp-messenger 1.0.33 → 1.0.35

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/.env.example CHANGED
@@ -1,18 +1,18 @@
1
- # ---------------------------------------------------------------------------
2
- # MCP Messenger — configuration
3
- # Deux modes, deux jeux de variables. Une même installation peut servir aux deux.
4
- # ---------------------------------------------------------------------------
5
-
6
- # === Mode serveur (boîte aux lettres) : `mcp-messenger --server` ===========
7
-
8
- # Port d'écoute. L'interface, elle, n'est pas configurable : le serveur écoute
9
- # toujours sur 127.0.0.1 et refuse de démarrer si MESSENGER_HOST vaut autre chose.
10
- # Pour l'atteindre depuis une autre machine, placer un reverse proxy devant.
11
- MESSENGER_PORT=3600
12
-
13
- # Chemin du fichier SQLite (défaut : <cwd>/data/messenger.db).
14
- # MESSENGER_DB_PATH=/var/lib/mcp-messenger/messenger.db
15
-
1
+ # ---------------------------------------------------------------------------
2
+ # MCP Messenger — configuration
3
+ # Deux modes, deux jeux de variables. Une même installation peut servir aux deux.
4
+ # ---------------------------------------------------------------------------
5
+
6
+ # === Mode serveur (boîte aux lettres) : `mcp-messenger --server` ===========
7
+
8
+ # Port d'écoute. L'interface, elle, n'est pas configurable : le serveur écoute
9
+ # toujours sur 127.0.0.1 et refuse de démarrer si MESSENGER_HOST vaut autre chose.
10
+ # Pour l'atteindre depuis une autre machine, placer un reverse proxy devant.
11
+ MESSENGER_PORT=3600
12
+
13
+ # Chemin du fichier SQLite (défaut : <cwd>/data/messenger.db).
14
+ # MESSENGER_DB_PATH=/var/lib/mcp-messenger/messenger.db
15
+
16
16
  # Aucun secret à poser ici : au premier démarrage, le serveur écrit un code d'installation
17
17
  # dans son journal ; la console (/ui) le demande pour créer le compte administrateur
18
18
  # (nom d'utilisateur + mot de passe). Les clés des agents se créent ensuite dans la
@@ -20,49 +20,50 @@ MESSENGER_PORT=3600
20
20
  # MESSENGER_ADMIN_TOKEN ne sont plus lus : le serveur refuse de démarrer s'ils sont posés.
21
21
  # Mot de passe perdu : npx @imenam/mcp-messenger --reset-password
22
22
 
23
- # Rate limiting (par IP).
24
- MESSENGER_RATE_LIMIT_MAX=600
25
- MESSENGER_RATE_LIMIT_WINDOW_MINUTES=15
26
-
27
- # Purge automatique de la corbeille (0 = désactivée).
28
- MESSENGER_TRASH_RETENTION_DAYS=30
29
-
30
- # === Mode MCP (côté agent) : `mcp-messenger` ==============================
31
-
32
- # Adresse FIXE de l'agent — à laisser vide dans la plupart des cas.
33
- # Vide : chaque session reçoit sa propre boîte (`box_…`), allouée au démarrage. Deux
34
- # sessions ouvertes sur le même projet ne partagent donc pas leur courrier. L'agent
35
- # donne son adresse avec `whoami`, et reprend une boîte existante avec
36
- # `attach_to_mailbox`.
37
- # Renseignée : toutes les sessions utilisant cette configuration partagent cette boîte.
38
- # MESSENGER_AGENT_ID=
39
-
40
- # Adresse du serveur boîte aux lettres (http ou https, local ou distant).
41
- MESSENGER_SERVER_URL=http://127.0.0.1:3600
42
-
23
+ # Rate limiting (par IP).
24
+ MESSENGER_RATE_LIMIT_MAX=600
25
+ MESSENGER_RATE_LIMIT_WINDOW_MINUTES=15
26
+
27
+ # Purge automatique de la corbeille (0 = désactivée).
28
+ MESSENGER_TRASH_RETENTION_DAYS=30
29
+
30
+ # === Mode MCP (côté agent) : `mcp-messenger` ==============================
31
+
32
+ # Adresse FIXE de l'agent — à laisser vide dans la plupart des cas.
33
+ # Vide : chaque session reçoit sa propre boîte (`box_…`), allouée au démarrage. Deux
34
+ # sessions ouvertes sur le même projet ne partagent donc pas leur courrier. L'agent
35
+ # donne son adresse avec `whoami`, et reprend une boîte existante avec
36
+ # `attach_to_mailbox`.
37
+ # Renseignée : toutes les sessions utilisant cette configuration partagent cette boîte.
38
+ # MESSENGER_AGENT_ID=
39
+
40
+ # Adresse du serveur boîte aux lettres (http ou https, local ou distant).
41
+ MESSENGER_SERVER_URL=http://127.0.0.1:3600
42
+
43
43
  # Clé API créée dans la console (onglet Clés API) — obligatoire.
44
44
  MESSENGER_API_KEY=mk_change-me
45
45
 
46
- # Métadonnées publiées dans l'annuaire des agents (facultatif). Le nom d'agent ne sert
47
- # qu'avec une clé valable pour tous les agents : une clé liée à un agent donne le sien.
48
- # MESSENGER_AGENT_NAME=Planner
49
- # MESSENGER_AGENT_DESCRIPTION=Agent de planification, répond aux demandes de découpage de tâches.
50
-
51
- # Délai maximal d'un appel HTTP vers le serveur (ms).
52
- MESSENGER_TIMEOUT_MS=15000
53
-
54
- # Reprise de la boîte d'une session interrompue puis relancée.
55
- # auto (défaut) : la session retrouve sa boîte quand c'est certain (l'hôte a redonné le
56
- # même identifiant de session) ou sans ambiguïté (une seule boîte du projet n'est plus
57
- # tenue par personne, et elle a servi).
58
- # ask : rien n'est repris tout seul ; `whoami` présente les boîtes en attente, l'agent en
59
- # rattache une avec `attach_to_mailbox`.
60
- # off : chaque session repart d'une boîte neuve.
61
- # Une boîte tenue par une session vivante n'est jamais reprise, quel que soit le réglage.
62
- # MESSENGER_RESUME=auto
63
-
64
- # Identifiant de la session hôte, celui qui relie une session reprise à celle qu'elle
65
- # continue. Inutile à renseigner : il est lu dans l'environnement hérité de l'hôte
66
- # (CLAUDE_CODE_SESSION_ID et équivalents). À poser soi-même pour des sessions lancées par
67
- # script. Il n'est jamais écrit sur le disque en clair, seule son empreinte l'est.
68
- # MESSENGER_SESSION_KEY=
46
+ # Métadonnées publiées dans l'annuaire des agents (facultatif). Elles ne servent qu'avec
47
+ # une clé valable pour tous les agents : une clé liée à un agent porte son nom et sa
48
+ # description, et refuse d'autres valeurs.
49
+ # MESSENGER_AGENT_NAME=Planner
50
+ # MESSENGER_AGENT_DESCRIPTION=Agent de planification, répond aux demandes de découpage de tâches.
51
+
52
+ # Délai maximal d'un appel HTTP vers le serveur (ms).
53
+ MESSENGER_TIMEOUT_MS=15000
54
+
55
+ # Reprise de la boîte d'une session interrompue puis relancée.
56
+ # auto (défaut) : la session retrouve sa boîte quand c'est certain (l'hôte a redonné le
57
+ # même identifiant de session) ou sans ambiguïté (une seule boîte du projet n'est plus
58
+ # tenue par personne, et elle a servi).
59
+ # ask : rien n'est repris tout seul ; `whoami` présente les boîtes en attente, l'agent en
60
+ # rattache une avec `attach_to_mailbox`.
61
+ # off : chaque session repart d'une boîte neuve.
62
+ # Une boîte tenue par une session vivante n'est jamais reprise, quel que soit le réglage.
63
+ # MESSENGER_RESUME=auto
64
+
65
+ # Identifiant de la session hôte, celui qui relie une session reprise à celle qu'elle
66
+ # continue. Inutile à renseigner : il est lu dans l'environnement hérité de l'hôte
67
+ # (CLAUDE_CODE_SESSION_ID et équivalents). À poser soi-même pour des sessions lancées par
68
+ # script. Il n'est jamais écrit sur le disque en clair, seule son empreinte l'est.
69
+ # MESSENGER_SESSION_KEY=
package/README.md CHANGED
@@ -72,12 +72,15 @@ Trois invariants, non négociables et vérifiés par les tests :
72
72
 
73
73
  Une clé se crée dans la console avec un nom, une portée et une durée :
74
74
 
75
- - **Un seul agent** : la clé porte un nom d'agent (par exemple `Documentor Dev`). Les
76
- boîtes ouvertes avec elle prennent ce nom — `MESSENGER_AGENT_NAME` devient inutile côté
77
- client, et un autre nom y est refusé — et elle n'ouvre que les boîtes de cet agent.
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é.
78
81
  - **Tous les agents** : la clé ouvre toutes les boîtes ; chaque client donne alors son nom
79
- dans `MESSENGER_AGENT_NAME`. C'est un mode « cercle de confiance », pour les sessions
80
- d'une même personne.
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.
81
84
  - **Durée** : 30 jours, 90 jours, 1 an ou sans échéance. La liste signale une clé qui expire
82
85
  dans les 7 jours ; on la prolonge depuis son détail.
83
86
 
@@ -138,6 +141,10 @@ Au démarrage, le MCP s'enregistre dans l'annuaire du serveur ; si le serveur es
138
141
  momentanément injoignable, la session MCP démarre quand même et l'enregistrement se fera au
139
142
  premier appel réussi.
140
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
+
141
148
  ### 3. Écrire à un autre agent
142
149
 
143
150
  L'agent A appelle `send_message` avec `to: ["coder"]`, l'agent B voit le message via
@@ -323,7 +330,7 @@ pas au même rythme :
323
330
  | | D'où vient-il ? | De quoi s'occupe-t-il ? |
324
331
  |---|---|---|
325
332
  | Champ | `agent_name`, `agent_description` | `display_name`, `description` |
326
- | Posé par | la configuration (`MESSENGER_AGENT_NAME`, `MESSENGER_AGENT_DESCRIPTION`) | l'agent lui-même, via `rename_mailbox` |
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` |
327
334
  | Change | jamais depuis une session | librement, au fil du travail |
328
335
  | Exemple | `Ai Corrector Dev` | `Correction du parseur de copies` |
329
336
 
@@ -650,7 +657,7 @@ dans la base. `MESSENGER_TOKEN`, `MESSENGER_AGENT_TOKENS` et `MESSENGER_ADMIN_TO
650
657
  | `MESSENGER_SERVER_URL` | ✅ | `http://` ou `https://`, local ou distant |
651
658
  | `MESSENGER_API_KEY` | ✅ | Clé API créée dans la console, présentée sur chaque appel |
652
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 |
653
- | `MESSENGER_AGENT_DESCRIPTION` | — | À quoi sert cet agent, pour ses correspondants |
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 |
654
661
  | `MESSENGER_TIMEOUT_MS` | `15000` | Délai maximal d'un appel HTTP |
655
662
  | `MESSENGER_STATE_DIR` | — | Répertoire des relais vers le veilleur (défaut : `~/.mcp-messenger`) |
656
663
 
@@ -699,8 +706,8 @@ cet agent (403 sinon). Les routes `/admin/*` n'acceptent que la session de la co
699
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 |
700
707
  | `DELETE` | `/admin/links/:id` | Supprime une liaison |
701
708
  | `GET` | `/admin/keys` | Clés API, sans leur secret : début et fin, agent, échéance, dernier usage, état (`active`, `expired`, `revoked`) |
702
- | `POST` | `/admin/keys` | Crée une clé (`name`, `agent_name` ou `null` pour tous les agents, `lifetime_days` ou `null`) — rend le secret, une seule fois |
703
- | `GET` `PATCH` | `/admin/keys/:id` | Détail d'une clé et boîtes ouvertes avec elle · renommer (`name`) |
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) |
704
711
  | `POST` | `/admin/keys/:id/extend` | Repousse l'échéance de `days` jours, ou la retire (`days: null`) |
705
712
  | `POST` | `/admin/keys/:id/rotate` | Nouvelle clé aux mêmes réglages ; l'ancienne reste acceptée 24 h |
706
713
  | `POST` | `/admin/keys/:id/revoke` | Révoque la clé, refusée dès l'appel suivant |
@@ -796,7 +803,7 @@ test/ # store, config, API HTTP, outils MCP, binaire réel
796
803
 
797
804
  ### Tests
798
805
 
799
- 379 tests couvrant : le compte de la console (création gardée par le code d'installation,
806
+ 382 tests couvrant : le compte de la console (création gardée par le code d'installation,
800
807
  connexion et limitation des essais, changement de mot de passe qui ferme les autres
801
808
  sessions, fermeture d'une session), les clés API (montrées une fois, liées à un agent ou
802
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
@@ -17,6 +17,8 @@ export interface ApiKey {
17
17
  suffix: string;
18
18
  /** Agent dont la clé ouvre les boîtes ; null = tous les agents. */
19
19
  agent_name: string | null;
20
+ /** Description de cet agent, portée par la clé ; seule une clé liée à un agent en a une. */
21
+ agent_description: string | null;
20
22
  created_at: string;
21
23
  created_by: string | null;
22
24
  expires_at: string | null;
@@ -93,6 +95,7 @@ export declare class AccessStore {
93
95
  createApiKey(input: {
94
96
  name: string;
95
97
  agentName: string | null;
98
+ agentDescription?: string | null;
96
99
  lifetimeDays: number | null;
97
100
  createdBy: string | null;
98
101
  secret: string;
@@ -101,6 +104,11 @@ export declare class AccessStore {
101
104
  getApiKey(id: string): ApiKey | null;
102
105
  private requireKey;
103
106
  renameApiKey(id: string, name: string): ApiKey;
107
+ /**
108
+ * Change la description de l'agent portée par une clé liée à un agent, et la reporte sur
109
+ * les boîtes déjà ouvertes avec elle. null l'efface de la clé, pas des boîtes.
110
+ */
111
+ setApiKeyAgentDescription(id: string, description: string | null): ApiKey;
104
112
  /**
105
113
  * Repousse l'expiration de `days` jours, comptés depuis l'échéance encore à venir ou,
106
114
  * pour une clé déjà expirée, depuis maintenant. `null` retire toute échéance.
@@ -108,7 +116,7 @@ export declare class AccessStore {
108
116
  extendApiKey(id: string, days: number | null): ApiKey;
109
117
  revokeApiKey(id: string): ApiKey;
110
118
  /**
111
- * Remplace une clé : la nouvelle reprend le nom, l'agent et la durée de validité de
119
+ * Remplace une clé : la nouvelle reprend le nom, l'agent, sa description et la durée de validité de
112
120
  * l'ancienne ; l'ancienne reste acceptée `graceMs` encore, au plus jusqu'à son échéance.
113
121
  */
114
122
  rotateApiKey(id: string, secret: string, createdBy: string | null, graceMs: number): {
@@ -4,7 +4,7 @@ import { MessengerError } from "./errors.js";
4
4
  /** Une date de dernier usage n'est réécrite qu'au-delà de ce délai : pas une écriture par requête. */
5
5
  const TOUCH_INTERVAL_MS = 60_000;
6
6
  const DAY_MS = 24 * 60 * 60 * 1000;
7
- const KEY_COLUMNS = `k.id, k.name, k.prefix, k.suffix, k.agent_name, k.created_at, k.created_by, k.expires_at,
7
+ const KEY_COLUMNS = `k.id, k.name, k.prefix, k.suffix, k.agent_name, k.agent_description, k.created_at, k.created_by, k.expires_at,
8
8
  k.lifetime_days, k.revoked_at, k.last_used_at, k.last_used_ip,
9
9
  (SELECT COUNT(*) FROM mailbox_keys mk JOIN agents a ON a.id = mk.mailbox WHERE mk.api_key_id = k.id) AS mailboxes`;
10
10
  export class AccessStore {
@@ -41,6 +41,7 @@ export class AccessStore {
41
41
  prefix TEXT NOT NULL,
42
42
  suffix TEXT NOT NULL,
43
43
  agent_name TEXT,
44
+ agent_description TEXT,
44
45
  created_at TEXT NOT NULL,
45
46
  created_by TEXT,
46
47
  expires_at TEXT,
@@ -183,9 +184,9 @@ export class AccessStore {
183
184
  const id = newId("key");
184
185
  const visible = input.secret.startsWith(API_KEY_PREFIX) ? API_KEY_PREFIX.length : 0;
185
186
  this.db
186
- .prepare(`INSERT INTO api_keys (id, name, secret_hash, prefix, suffix, agent_name, created_at, created_by, expires_at, lifetime_days)
187
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
188
- .run(id, input.name, digestSecret(input.secret), input.secret.slice(0, visible + 4), input.secret.slice(-4), input.agentName, this.iso(), input.createdBy, input.lifetimeDays === null ? null : this.iso(input.lifetimeDays * DAY_MS), input.lifetimeDays);
187
+ .prepare(`INSERT INTO api_keys (id, name, secret_hash, prefix, suffix, agent_name, agent_description, created_at, created_by, expires_at, lifetime_days)
188
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
189
+ .run(id, input.name, digestSecret(input.secret), input.secret.slice(0, visible + 4), input.secret.slice(-4), input.agentName, input.agentDescription ?? null, this.iso(), input.createdBy, input.lifetimeDays === null ? null : this.iso(input.lifetimeDays * DAY_MS), input.lifetimeDays);
189
190
  return this.getApiKey(id);
190
191
  }
191
192
  listApiKeys() {
@@ -206,6 +207,25 @@ export class AccessStore {
206
207
  this.db.prepare("UPDATE api_keys SET name = ? WHERE id = ?").run(name, id);
207
208
  return this.getApiKey(id);
208
209
  }
210
+ /**
211
+ * Change la description de l'agent portée par une clé liée à un agent, et la reporte sur
212
+ * les boîtes déjà ouvertes avec elle. null l'efface de la clé, pas des boîtes.
213
+ */
214
+ setApiKeyAgentDescription(id, description) {
215
+ const key = this.requireKey(id);
216
+ if (!key.agent_name) {
217
+ throw new MessengerError("only a key bound to an agent carries an agent description", 400);
218
+ }
219
+ this.db.transaction(() => {
220
+ this.db.prepare("UPDATE api_keys SET agent_description = ? WHERE id = ?").run(description, id);
221
+ if (description !== null) {
222
+ this.db
223
+ .prepare("UPDATE agents SET agent_description = ? WHERE id IN (SELECT mailbox FROM mailbox_keys WHERE api_key_id = ?)")
224
+ .run(description, id);
225
+ }
226
+ })();
227
+ return this.getApiKey(id);
228
+ }
209
229
  /**
210
230
  * Repousse l'expiration de `days` jours, comptés depuis l'échéance encore à venir ou,
211
231
  * pour une clé déjà expirée, depuis maintenant. `null` retire toute échéance.
@@ -227,7 +247,7 @@ export class AccessStore {
227
247
  return this.getApiKey(id);
228
248
  }
229
249
  /**
230
- * Remplace une clé : la nouvelle reprend le nom, l'agent et la durée de validité de
250
+ * Remplace une clé : la nouvelle reprend le nom, l'agent, sa description et la durée de validité de
231
251
  * l'ancienne ; l'ancienne reste acceptée `graceMs` encore, au plus jusqu'à son échéance.
232
252
  */
233
253
  rotateApiKey(id, secret, createdBy, graceMs) {
@@ -238,6 +258,7 @@ export class AccessStore {
238
258
  const key = this.createApiKey({
239
259
  name: old.name,
240
260
  agentName: old.agent_name,
261
+ agentDescription: old.agent_description,
241
262
  lifetimeDays: old.lifetime_days,
242
263
  createdBy,
243
264
  secret,
@@ -1,7 +1,7 @@
1
1
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
3
  import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
4
- import { loadAgentConfig } from "../config.js";
4
+ import { ConfigError, loadAgentConfig } from "../config.js";
5
5
  import { MessengerClient, MessengerClientError } from "../client/messenger-client.js";
6
6
  import { removeOwnHandoffs } from "../cli/watch-handoff.js";
7
7
  import { newId } from "../core/ids.js";
@@ -190,13 +190,23 @@ export async function runMcpMode() {
190
190
  const config = loadAgentConfig();
191
191
  const client = new MessengerClient(config);
192
192
  // Un serveur momentanément injoignable ne doit pas empêcher la session MCP de démarrer :
193
- // l'agent verra l'erreur au premier outil appelé, et pourra réessayer.
193
+ // l'agent verra l'erreur au premier outil appelé, et pourra réessayer. Un refus du
194
+ // serveur, lui, ne passera pas en réessayant : la clé ou la configuration est à corriger,
195
+ // et le démarrage s'arrête en le disant.
194
196
  try {
195
197
  const identity = await resolveIdentity(client, config);
196
198
  logger.info(`mailbox \`${identity.mailbox}\` ready on ${config.serverUrl}${describeIdentity(identity, config)}`);
197
199
  }
198
200
  catch (error) {
199
- logger.warn(`could not reach ${config.serverUrl}: ${error instanceof Error ? error.message : String(error)}`);
201
+ if (error instanceof MessengerClientError && (error.status === 401 || error.status === 403)) {
202
+ throw new ConfigError(`the messenger server at ${config.serverUrl} refused this configuration: ${error.message}`);
203
+ }
204
+ if (error instanceof MessengerClientError && error.status !== undefined) {
205
+ logger.warn(`mailbox registration failed on ${config.serverUrl} (HTTP ${error.status}): ${error.message}`);
206
+ }
207
+ else {
208
+ logger.warn(`could not reach ${config.serverUrl}: ${error instanceof Error ? error.message : String(error)}`);
209
+ }
200
210
  }
201
211
  // Tant que ce battement continue, la boîte de cette session n'est reprise par personne.
202
212
  // `unref` : c'est une trace, pas une raison de garder le processus en vie.
@@ -119,13 +119,32 @@ const passwordChangeSchema = z.object({
119
119
  });
120
120
  /** Durée de validité d'une clé, en jours ; null = sans échéance. */
121
121
  const lifetimeSchema = z.number().int().min(1).max(3650).nullable();
122
- const keyCreateSchema = z.object({
122
+ /** Description de l'agent portée par une clé : une chaîne vide l'efface. */
123
+ const agentDescriptionSchema = z
124
+ .string()
125
+ .trim()
126
+ .max(2000)
127
+ .nullable()
128
+ .transform((value) => value || null);
129
+ const keyCreateSchema = z
130
+ .object({
123
131
  name: z.string().trim().min(1).max(200),
124
132
  /** Agent dont la clé ouvre les boîtes ; null = tous les agents. */
125
133
  agent_name: z.string().trim().min(1).max(200).nullable(),
134
+ agent_description: agentDescriptionSchema.optional(),
126
135
  lifetime_days: lifetimeSchema,
136
+ })
137
+ .refine((input) => input.agent_name !== null || !input.agent_description, {
138
+ message: "only a key bound to an agent carries an agent description",
139
+ });
140
+ const keyPatchSchema = z
141
+ .object({
142
+ name: z.string().trim().min(1).max(200).optional(),
143
+ agent_description: agentDescriptionSchema.optional(),
144
+ })
145
+ .refine((input) => input.name !== undefined || input.agent_description !== undefined, {
146
+ message: "provide `name` and/or `agent_description`",
127
147
  });
128
- const keyRenameSchema = z.object({ name: z.string().trim().min(1).max(200) });
129
148
  const keyExtendSchema = z.object({ days: lifetimeSchema });
130
149
  const adminThreadsSchema = z.object({
131
150
  search: z.string().trim().min(1).optional(),
@@ -489,6 +508,7 @@ export function createApp({ store, config, setup = new SetupGate(store.access) }
489
508
  const key = access.createApiKey({
490
509
  name: input.name,
491
510
  agentName: input.agent_name,
511
+ agentDescription: input.agent_description ?? null,
492
512
  lifetimeDays: input.lifetime_days,
493
513
  createdBy: req.consoleUser.username,
494
514
  secret,
@@ -504,8 +524,12 @@ export function createApp({ store, config, setup = new SetupGate(store.access) }
504
524
  res.json({ key, mailboxes: access.mailboxesForKey(id) });
505
525
  });
506
526
  admin.patch("/keys/:id", (req, res) => {
507
- const input = keyRenameSchema.parse(req.body ?? {});
508
- res.json({ key: access.renameApiKey(String(req.params.id), input.name) });
527
+ const input = keyPatchSchema.parse(req.body ?? {});
528
+ const id = String(req.params.id);
529
+ let key = input.name !== undefined ? access.renameApiKey(id, input.name) : null;
530
+ if (input.agent_description !== undefined)
531
+ key = access.setApiKeyAgentDescription(id, input.agent_description);
532
+ res.json({ key });
509
533
  });
510
534
  admin.post("/keys/:id/extend", (req, res) => {
511
535
  const input = keyExtendSchema.parse(req.body ?? {});
@@ -550,17 +574,24 @@ export function createApp({ store, config, setup = new SetupGate(store.access) }
550
574
  if (input.fresh && store.getAgent(req.agentId)) {
551
575
  throw new MessengerError(`mailbox already exists: ${req.agentId}`, 409);
552
576
  }
553
- // Une clé liée à un agent donne son nom à la boîte. Un autre nom venu de la
554
- // configuration du client est une erreur de réglage, signalée plutôt que corrigée.
577
+ // Une clé liée à un agent donne son nom et sa description à la boîte. Une autre valeur
578
+ // venue de la configuration du client est une erreur de réglage, signalée plutôt que
579
+ // corrigée.
555
580
  const key = req.apiKey;
556
- if (key.agent_name && input.agent_name && input.agent_name !== key.agent_name) {
557
- throw new MessengerError(`this API key is bound to agent "${key.agent_name}": remove MESSENGER_AGENT_NAME from the client ` +
558
- `configuration, or set it to "${key.agent_name}"`, 403);
581
+ if (key.agent_name) {
582
+ if (input.agent_name && input.agent_name !== key.agent_name) {
583
+ throw new MessengerError(`this API key is bound to agent "${key.agent_name}": remove MESSENGER_AGENT_NAME from the client ` +
584
+ `configuration, or set it to "${key.agent_name}"`, 403);
585
+ }
586
+ if (input.agent_description && input.agent_description !== key.agent_description) {
587
+ throw new MessengerError(`this API key carries the description of agent "${key.agent_name}": remove MESSENGER_AGENT_DESCRIPTION ` +
588
+ "from the client configuration, and edit the description in the web console", 403);
589
+ }
559
590
  }
560
591
  const agent = store.ensureAgent(req.agentId, {
561
592
  registered: true,
562
593
  agent_name: key.agent_name ?? input.agent_name ?? null,
563
- agent_description: input.agent_description ?? null,
594
+ agent_description: key.agent_name ? key.agent_description : (input.agent_description ?? null),
564
595
  // Un client antérieur à la séparation agent / boîte n'envoie que ces deux-là.
565
596
  display_name: input.display_name ?? null,
566
597
  description: input.description ?? null,
@@ -2695,6 +2695,15 @@ function formField(label, control, hint) {
2695
2695
  return wrapper;
2696
2696
  }
2697
2697
 
2698
+ function textArea(value, placeholder) {
2699
+ const area = document.createElement("textarea");
2700
+ area.value = value || "";
2701
+ area.rows = 2;
2702
+ area.maxLength = 2000;
2703
+ if (placeholder) area.placeholder = placeholder;
2704
+ return area;
2705
+ }
2706
+
2698
2707
  function textInput(value, options = {}) {
2699
2708
  const input = document.createElement("input");
2700
2709
  input.type = options.type || "text";
@@ -2978,11 +2987,15 @@ function choiceChips(options, selected, onPick) {
2978
2987
 
2979
2988
  function openKeyCreate() {
2980
2989
  keysView.detailId = null;
2981
- const form = { name: "", scope: "one", agent: "", lifetime: KEY_LIFETIMES[1] };
2990
+ const form = { name: "", scope: "one", agent: "", description: "", lifetime: KEY_LIFETIMES[1] };
2982
2991
  const names = knownAgentNames();
2983
2992
 
2984
2993
  const nameInput = textInput("", { placeholder: "Poste, machine ou usage", autocomplete: "off" });
2985
2994
  const agentInput = textInput("", { placeholder: "Nom de l'agent", autocomplete: "off", list: "key-agent-names" });
2995
+ const descriptionInput = textArea("", "À quoi sert cet agent, pour ses correspondants");
2996
+ descriptionInput.addEventListener("input", () => {
2997
+ form.description = descriptionInput.value;
2998
+ });
2986
2999
  const datalist = node("datalist");
2987
3000
  datalist.id = "key-agent-names";
2988
3001
  for (const name of names) {
@@ -3027,11 +3040,14 @@ function openKeyCreate() {
3027
3040
  box.append(agentMark, agentInput, newTag);
3028
3041
  updateAgentMark();
3029
3042
  agentField.appendChild(box);
3043
+ const description = node("span", "field-input field-input--area");
3044
+ description.appendChild(descriptionInput);
3045
+ agentField.appendChild(description);
3030
3046
  agentField.appendChild(
3031
3047
  node(
3032
3048
  "span",
3033
3049
  "field-hint",
3034
- "Les sessions ouvertes avec cette clé portent ce nom d'agent : MESSENGER_AGENT_NAME n'a plus à être posé côté client."
3050
+ "Le nom et la description s'appliquent à toutes les sessions ouvertes avec cette clé : rien à poser côté client."
3035
3051
  )
3036
3052
  );
3037
3053
  } else {
@@ -3071,7 +3087,7 @@ function openKeyCreate() {
3071
3087
  datalist
3072
3088
  );
3073
3089
  // Le volet se reconstruit à chaque choix : la saisie en cours garde le curseur.
3074
- if (focused === agentInput || focused === nameInput) focused.focus();
3090
+ if (focused === agentInput || focused === nameInput || focused === descriptionInput) focused.focus();
3075
3091
  create.disabled = !form.name.trim() || (form.scope === "one" && !form.agent.trim());
3076
3092
  }
3077
3093
 
@@ -3097,6 +3113,7 @@ function openKeyCreate() {
3097
3113
  const created = await post("/admin/keys", {
3098
3114
  name: form.name.trim(),
3099
3115
  agent_name: form.scope === "one" ? form.agent.trim() : null,
3116
+ ...(form.scope === "one" && form.description.trim() ? { agent_description: form.description.trim() } : {}),
3100
3117
  lifetime_days: form.lifetime.days,
3101
3118
  });
3102
3119
  renderKeyReveal(created.key, created.secret, null);
@@ -3247,6 +3264,7 @@ async function openKeyDetail(id) {
3247
3264
 
3248
3265
  function renderKeyDetail({ key, mailboxes }) {
3249
3266
  let renaming = false;
3267
+ let describing = false;
3250
3268
  let extendOpen = false;
3251
3269
  const error = node("p", "form-banner");
3252
3270
  error.hidden = true;
@@ -3368,6 +3386,7 @@ function renderKeyDetail({ key, mailboxes }) {
3368
3386
  list.appendChild(cell);
3369
3387
  };
3370
3388
  add("Agent autorisé", keyAgent(key));
3389
+ if (key.agent_name) add("Description", descriptionCell());
3371
3390
  add("Créée", `${formatDay(key.created_at)}${key.created_by ? ` par ${key.created_by}` : ""}`);
3372
3391
  add(
3373
3392
  "Dernière utilisation",
@@ -3379,6 +3398,60 @@ function renderKeyDetail({ key, mailboxes }) {
3379
3398
  return list;
3380
3399
  }
3381
3400
 
3401
+ /** Description de l'agent, modifiable sur place : Entrée enregistre, Échap annule. */
3402
+ function descriptionCell() {
3403
+ if (!describing) {
3404
+ const value = node("span", key.agent_description ? "props-value" : "props-value key-muted", key.agent_description || "aucune");
3405
+ const edit = node("button", "btn btn--icon btn--ghost props-edit");
3406
+ edit.type = "button";
3407
+ edit.title = "Modifier la description";
3408
+ edit.setAttribute("aria-label", "Modifier la description de l'agent");
3409
+ edit.appendChild(icon("pencil"));
3410
+ edit.addEventListener("click", () => {
3411
+ describing = true;
3412
+ render();
3413
+ });
3414
+ return [value, edit];
3415
+ }
3416
+ const area = textArea(key.agent_description, "À quoi sert cet agent, pour ses correspondants");
3417
+ area.className = "inline-area";
3418
+ let saved = false;
3419
+ const save = async () => {
3420
+ if (saved) return;
3421
+ saved = true;
3422
+ const description = area.value.trim();
3423
+ if (description === (key.agent_description || "")) {
3424
+ describing = false;
3425
+ render();
3426
+ return;
3427
+ }
3428
+ try {
3429
+ await patch(`/admin/keys/${encodeURIComponent(key.id)}`, { agent_description: description || null });
3430
+ await reload();
3431
+ } catch (failure) {
3432
+ saved = false;
3433
+ fail(failure);
3434
+ }
3435
+ };
3436
+ area.addEventListener("keydown", (event) => {
3437
+ if (event.key === "Enter" && !event.shiftKey) {
3438
+ event.preventDefault();
3439
+ save();
3440
+ }
3441
+ if (event.key === "Escape") {
3442
+ // Échap referme la saisie, pas le volet.
3443
+ event.preventDefault();
3444
+ event.stopPropagation();
3445
+ saved = true;
3446
+ describing = false;
3447
+ render();
3448
+ }
3449
+ });
3450
+ area.addEventListener("blur", save);
3451
+ setTimeout(() => area.focus(), 0);
3452
+ return [area];
3453
+ }
3454
+
3382
3455
  function boxes() {
3383
3456
  const section = node("section", "drawer-section");
3384
3457
  const head = node("div", "drawer-section-head");
@@ -2780,6 +2780,43 @@ body {
2780
2780
  gap: 10px;
2781
2781
  }
2782
2782
 
2783
+ .field-input--area {
2784
+ height: auto;
2785
+ margin-top: 12px;
2786
+ padding: 8px 12px;
2787
+ }
2788
+
2789
+ .field-input textarea,
2790
+ .inline-area {
2791
+ flex: 1;
2792
+ min-width: 0;
2793
+ border: 0;
2794
+ background: none;
2795
+ font: inherit;
2796
+ font-size: 13px;
2797
+ line-height: 1.4;
2798
+ color: var(--text);
2799
+ resize: vertical;
2800
+ outline: none;
2801
+ }
2802
+
2803
+ .inline-area {
2804
+ width: 100%;
2805
+ padding: 6px 10px;
2806
+ border: 1px solid var(--accent);
2807
+ border-radius: var(--radius);
2808
+ background: var(--panel);
2809
+ }
2810
+
2811
+ .props-value {
2812
+ line-height: 1.4;
2813
+ }
2814
+
2815
+ .props-edit {
2816
+ flex: none;
2817
+ padding: 4px;
2818
+ }
2819
+
2783
2820
  .field-tag {
2784
2821
  font-size: 12px;
2785
2822
  color: var(--accent);
package/package.json CHANGED
@@ -1,61 +1,61 @@
1
- {
2
- "name": "@imenam/mcp-messenger",
3
- "version": "1.0.33",
4
- "main": "dist/src/index.js",
5
- "scripts": {
6
- "test": "npm run build && vitest run",
7
- "build": "tsc && node scripts/copy-assets.js",
8
- "start": "node dist/src/index.js",
9
- "serve": "node dist/src/index.js --server",
10
- "prepublishOnly": "npm run build",
11
- "release": "npm version patch && npm publish && git push --follow-tags",
12
- "typecheck": "tsc --noEmit",
13
- "test:watch": "vitest",
14
- "test:coverage": "vitest run --coverage"
15
- },
16
- "keywords": [
17
- "mcp",
18
- "model-context-protocol",
19
- "agent",
20
- "messaging",
21
- "mailbox",
22
- "agent-to-agent"
23
- ],
24
- "author": "Matthieu Pesnot-Pin <matthieupin@gmail.com>",
25
- "license": "ISC",
26
- "description": "Cross-platform agent-to-agent messaging: an MCP mailbox client plus its HTTP mailbox server.",
27
- "type": "module",
28
- "bin": {
29
- "mcp-messenger": "dist/src/index.js"
30
- },
31
- "engines": {
32
- "node": ">=20"
33
- },
34
- "publishConfig": {
35
- "access": "public"
36
- },
37
- "dependencies": {
38
- "@modelcontextprotocol/sdk": "^1.30.0",
39
- "better-sqlite3": "^12.11.1",
40
- "dotenv": "^17.4.2",
41
- "express": "^5.2.1",
42
- "express-rate-limit": "^8.7.0",
43
- "helmet": "^8.3.0",
44
- "zod": "^4.5.4"
45
- },
46
- "devDependencies": {
47
- "@types/better-sqlite3": "^7.6.13",
48
- "@types/express": "^5.0.6",
49
- "@types/node": "^22.20.1",
50
- "@vitest/coverage-v8": "^3.2.7",
51
- "jsdom": "^29.1.1",
52
- "typescript": "^5.9.3",
53
- "vitest": "^3.2.7"
54
- },
55
- "files": [
56
- "dist/src",
57
- "README.md",
58
- ".env.example",
59
- "LICENSE"
60
- ]
61
- }
1
+ {
2
+ "name": "@imenam/mcp-messenger",
3
+ "version": "1.0.35",
4
+ "main": "dist/src/index.js",
5
+ "scripts": {
6
+ "test": "npm run build && vitest run",
7
+ "build": "tsc && node scripts/copy-assets.js",
8
+ "start": "node dist/src/index.js",
9
+ "serve": "node dist/src/index.js --server",
10
+ "prepublishOnly": "npm run build",
11
+ "release": "npm version patch && npm publish && git push --follow-tags",
12
+ "typecheck": "tsc --noEmit",
13
+ "test:watch": "vitest",
14
+ "test:coverage": "vitest run --coverage"
15
+ },
16
+ "keywords": [
17
+ "mcp",
18
+ "model-context-protocol",
19
+ "agent",
20
+ "messaging",
21
+ "mailbox",
22
+ "agent-to-agent"
23
+ ],
24
+ "author": "Matthieu Pesnot-Pin <matthieupin@gmail.com>",
25
+ "license": "ISC",
26
+ "description": "Cross-platform agent-to-agent messaging: an MCP mailbox client plus its HTTP mailbox server.",
27
+ "type": "module",
28
+ "bin": {
29
+ "mcp-messenger": "dist/src/index.js"
30
+ },
31
+ "engines": {
32
+ "node": ">=20"
33
+ },
34
+ "publishConfig": {
35
+ "access": "public"
36
+ },
37
+ "dependencies": {
38
+ "@modelcontextprotocol/sdk": "^1.30.0",
39
+ "better-sqlite3": "^12.11.1",
40
+ "dotenv": "^17.4.2",
41
+ "express": "^5.2.1",
42
+ "express-rate-limit": "^8.7.0",
43
+ "helmet": "^8.3.0",
44
+ "zod": "^4.5.4"
45
+ },
46
+ "devDependencies": {
47
+ "@types/better-sqlite3": "^7.6.13",
48
+ "@types/express": "^5.0.6",
49
+ "@types/node": "^22.20.1",
50
+ "@vitest/coverage-v8": "^5.0.5",
51
+ "jsdom": "^29.1.1",
52
+ "typescript": "^5.9.3",
53
+ "vitest": "^5.0.5"
54
+ },
55
+ "files": [
56
+ "dist/src",
57
+ "README.md",
58
+ ".env.example",
59
+ "LICENSE"
60
+ ]
61
+ }