@imenam/mcp-messenger 1.0.6 → 1.0.8
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 +6 -3
- package/README.md +53 -13
- package/dist/src/cli/setup-mcp.js +1 -1
- package/dist/src/cli/watch.js +2 -2
- package/dist/src/client/messenger-client.d.ts +2 -3
- package/dist/src/client/messenger-client.js +3 -3
- package/dist/src/config.d.ts +3 -2
- package/dist/src/config.js +2 -2
- package/dist/src/core/store.d.ts +23 -8
- package/dist/src/core/store.js +65 -42
- package/dist/src/core/types.d.ts +19 -9
- package/dist/src/core/types.js +2 -2
- package/dist/src/mcp/start.js +21 -1
- package/dist/src/mcp/tools.js +122 -63
- package/dist/src/server/app.js +18 -14
- package/dist/src/server/ui/app.js +320 -318
- package/dist/src/server/ui/styles.css +5 -0
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/package.json +1 -1
package/.env.example
CHANGED
|
@@ -13,7 +13,8 @@ MESSENGER_PORT=3600
|
|
|
13
13
|
# Chemin du fichier SQLite (défaut : <cwd>/data/messenger.db).
|
|
14
14
|
# MESSENGER_DB_PATH=/var/lib/mcp-messenger/messenger.db
|
|
15
15
|
|
|
16
|
-
# Jeton exigé sur TOUTE requête
|
|
16
|
+
# Jeton exigé sur TOUTE requête de données (API, /health, données de la console).
|
|
17
|
+
# Seule la page de la console (/ui), qui ne contient aucun secret, est servie sans lui.
|
|
17
18
|
# OBLIGATOIRE : sans MESSENGER_TOKEN ni MESSENGER_AGENT_TOKENS, le serveur refuse
|
|
18
19
|
# de démarrer. Générer avec : npx @imenam/mcp-messenger --generate-token
|
|
19
20
|
MESSENGER_TOKEN=change-me
|
|
@@ -23,8 +24,10 @@ MESSENGER_TOKEN=change-me
|
|
|
23
24
|
# Format : agent_id:token,agent_id:token
|
|
24
25
|
# MESSENGER_AGENT_TOKENS=planner:tok-planner,coder:tok-coder
|
|
25
26
|
|
|
26
|
-
# Jeton de la console web
|
|
27
|
-
#
|
|
27
|
+
# Jeton de la console web, saisi une fois dans son formulaire (/ui) : le serveur ouvre
|
|
28
|
+
# alors une session de 12 h dans un cookie HttpOnly, sans jamais y mettre le jeton.
|
|
29
|
+
# À défaut, MESSENGER_TOKEN fait office ; il devient obligatoire si seul
|
|
30
|
+
# MESSENGER_AGENT_TOKENS est utilisé.
|
|
28
31
|
# MESSENGER_ADMIN_TOKEN=change-me-too
|
|
29
32
|
|
|
30
33
|
# Rate limiting (par IP).
|
package/README.md
CHANGED
|
@@ -97,7 +97,7 @@ la session MCP démarre quand même et l'enregistrement se fera au premier appel
|
|
|
97
97
|
|
|
98
98
|
L'agent A appelle `send_message` avec `to: ["coder"]`, l'agent B voit le message via
|
|
99
99
|
`list_messages` puis `read_message`, et répond avec `reply_to` pour rester dans le fil.
|
|
100
|
-
Le fil se nomme au passage (`
|
|
100
|
+
Le fil se nomme au passage (`thread_name` sur `send_message`, ou `rename_thread` plus tard).
|
|
101
101
|
`wait_for_message` permet d'attendre une réponse sans faire tourner une boucle d'appels : il ne
|
|
102
102
|
rend la main que sur un message arrivé *après* le début de l'attente, les non-lus déjà en boîte
|
|
103
103
|
ne l'interrompent pas.
|
|
@@ -177,10 +177,11 @@ superviseur capable de lire les lignes d'un processus peut la brancher. Sans rel
|
|
|
177
177
|
|---|---|
|
|
178
178
|
| `whoami` | Adresse de cette session, serveur connecté, veilleur armé ou non, compteurs par dossier |
|
|
179
179
|
| `attach_to_mailbox` | Se brancher sur une autre boîte via son adresse ; sans argument, retour à la sienne |
|
|
180
|
-
| `
|
|
180
|
+
| `rename_mailbox` | Nommer ou renommer sa boîte (de quoi la session s'occupe) ; le nom de l'agent, posé par la configuration, reste |
|
|
181
|
+
| `rename_thread` | Nommer ou renommer un fil (`thread_id`) ; le nom est partagé par tous ses participants |
|
|
181
182
|
| `list_threads` | Carte des conversations : nom, participants, non-lus, dernière activité |
|
|
182
|
-
| `list_agents` | Annuaire : qui est joignable, du plus récemment actif au plus ancien |
|
|
183
|
-
| `send_message` |
|
|
183
|
+
| `list_agents` | Annuaire : qui est joignable, du plus récemment actif au plus ancien ; la boîte de l'appelant n'y figure pas |
|
|
184
|
+
| `send_message` | Écrire à un agent — un seul destinataire par message (`to`, `priority`, `reply_to`, `thread_name`) |
|
|
184
185
|
| `list_messages` | Lister la boîte (filtres : dossier, non lus, expéditeur, fil, texte libre) |
|
|
185
186
|
| `read_message` | Lire un message en entier ; le marque lu par défaut |
|
|
186
187
|
| `get_thread` | Relire toute une conversation, du plus ancien au plus récent |
|
|
@@ -194,12 +195,18 @@ superviseur capable de lire les lignes d'un processus peut la brancher. Sans rel
|
|
|
194
195
|
|
|
195
196
|
**Noms lisibles** — voir « Nommer les boîtes et les fils » ci-dessous.
|
|
196
197
|
|
|
198
|
+
**Un message, deux boîtes** — un échange lie exactement deux agents *distincts* : `to` prend
|
|
199
|
+
une seule adresse, il n'y a ni diffusion ni copie, et une boîte ne peut pas s'écrire à
|
|
200
|
+
elle-même. Pour toucher plusieurs agents, on leur envoie un message chacun, dans son propre
|
|
201
|
+
fil. C'est aussi pourquoi `list_agents` ne montre jamais l'appelant : la seule adresse qu'il
|
|
202
|
+
ne peut pas viser n'a pas à figurer dans son annuaire.
|
|
203
|
+
|
|
197
204
|
**Dossiers système** — toujours présents, non supprimables : `inbox`, `sent`, `archive`,
|
|
198
205
|
`trash`. Les dossiers personnalisés sont propres à chaque boîte.
|
|
199
206
|
|
|
200
207
|
**Ce que voit un agent** : uniquement les livraisons de la boîte à laquelle il est attaché. Un message adressé à `coder`
|
|
201
208
|
est invisible pour tout autre agent, y compris via son identifiant. Supprimer un message ne
|
|
202
|
-
touche que sa propre copie :
|
|
209
|
+
touche que sa propre copie : l'autre partie garde la sienne.
|
|
203
210
|
|
|
204
211
|
---
|
|
205
212
|
|
|
@@ -221,25 +228,58 @@ ont deux boîtes distinctes et ne se volent pas leur courrier.
|
|
|
221
228
|
2. Transmets-la à l'agent B, qui l'utilise dans `send_message`.
|
|
222
229
|
|
|
223
230
|
Comme les adresses sont opaques, demande aux agents de nommer leur boîte
|
|
224
|
-
(`
|
|
231
|
+
(`rename_mailbox`) : le nom apparaît dans `list_agents` et dans la console web, ce
|
|
225
232
|
qui permet de retrouver une adresse perdue.
|
|
226
233
|
|
|
234
|
+
### Deux noms par boîte : l'agent et la boîte
|
|
235
|
+
|
|
236
|
+
Une boîte porte deux identités, qui répondent à deux questions différentes et ne changent
|
|
237
|
+
pas au même rythme :
|
|
238
|
+
|
|
239
|
+
| | D'où vient-il ? | De quoi s'occupe-t-il ? |
|
|
240
|
+
|---|---|---|
|
|
241
|
+
| Champ | `agent_name`, `agent_description` | `display_name`, `description` |
|
|
242
|
+
| Posé par | la configuration (`MESSENGER_AGENT_NAME`, `MESSENGER_AGENT_DESCRIPTION`) | l'agent lui-même, via `rename_mailbox` |
|
|
243
|
+
| Change | jamais depuis une session | librement, au fil du travail |
|
|
244
|
+
| Exemple | `Ai Corrector Dev` | `Correction du parseur de copies` |
|
|
245
|
+
|
|
246
|
+
Le nom de l'agent seul ne distingue pas deux sessions ouvertes sur le même projet ; le nom
|
|
247
|
+
de la boîte seul ne dit pas d'où l'agent écrit. L'annuaire (`list_agents`), `whoami` et la
|
|
248
|
+
console web montrent donc toujours les deux, sous la forme `Agent · Boîte`. Renommer la
|
|
249
|
+
boîte ne touche jamais au nom de l'agent.
|
|
250
|
+
|
|
251
|
+
**Un nom d'agent ne désigne personne.** C'est le piège de ce modèle : plusieurs boîtes
|
|
252
|
+
portent le même `agent_name`, une par session lancée depuis la même configuration. Un agent
|
|
253
|
+
qui croise son propre nom d'agent dans l'annuaire regarde une *autre* session du même
|
|
254
|
+
projet, jamais la sienne — sa boîte, elle, n'y figure pas du tout. Seule l'adresse identifie
|
|
255
|
+
une session. Ce rappel est servi à trois endroits, du plus tôt au plus tard : les
|
|
256
|
+
instructions du serveur MCP (reçues à la connexion, avant le premier outil), la ligne qui
|
|
257
|
+
suit `Agent:` dans `whoami`, et le pied de l'annuaire dans `list_agents`.
|
|
258
|
+
|
|
259
|
+
**Les adresses ne se recopient pas à la main.** Le serveur attache l'expéditeur et le
|
|
260
|
+
destinataire à chaque message, et le lecteur les voit dans l'en-tête. Écrire son adresse
|
|
261
|
+
dans le corps n'ajoute rien et se met à mentir dès qu'elle est erronée ou périmée —
|
|
262
|
+
`send_message` le dit dans sa description et dans celle de `body`.
|
|
263
|
+
|
|
227
264
|
### Nommer les boîtes et les fils
|
|
228
265
|
|
|
229
266
|
Les identifiants (`box_…`, `thr_…`) ne disent rien de ce qui se passe. Deux noms en langue
|
|
230
|
-
naturelle viennent par-dessus,
|
|
267
|
+
naturelle viennent par-dessus, un outil pour chacun :
|
|
231
268
|
|
|
232
269
|
```
|
|
233
|
-
|
|
234
|
-
→ nomme la boîte de la session
|
|
270
|
+
rename_mailbox(name: "Développement des tests unitaires du parser")
|
|
271
|
+
→ nomme la boîte de la session (le nom de l'agent reste)
|
|
235
272
|
|
|
236
|
-
|
|
273
|
+
rename_thread(thread_id: "thr_…", name: "Le test échoue sur un bug applicatif")
|
|
237
274
|
→ nomme le fil
|
|
238
275
|
```
|
|
239
276
|
|
|
240
|
-
Le plus simple reste de nommer un fil au moment où on l'ouvre, via `
|
|
277
|
+
Le plus simple reste de nommer un fil au moment où on l'ouvre, via `thread_name` sur
|
|
241
278
|
`send_message` : l'agent qui écrit le premier message sait déjà de quoi il parle.
|
|
242
279
|
|
|
280
|
+
L'ancien outil `set_label`, qui cumulait les deux rôles, reste accepté sans être publié :
|
|
281
|
+
une session lancée avant la mise à jour peut encore l'appeler.
|
|
282
|
+
|
|
243
283
|
Un fil n'a **qu'un seul nom, partagé par tous ses participants** — ils discutent du même
|
|
244
284
|
sujet, ils doivent lire le même intitulé. N'importe quel participant peut le renommer, et
|
|
245
285
|
le dernier a le dernier mot ; le serveur retient qui (`named_by`). Un agent extérieur au
|
|
@@ -325,7 +365,7 @@ Toutes les variables sont documentées dans [`.env.example`](.env.example). L'es
|
|
|
325
365
|
| `MESSENGER_AGENT_ID` | — | Adresse **fixe**. Vide (recommandé) = une boîte par session |
|
|
326
366
|
| `MESSENGER_SERVER_URL` | ✅ | `http://` ou `https://`, local ou distant |
|
|
327
367
|
| `MESSENGER_TOKEN` | ✅ | Jeton présenté au serveur, sur chaque appel |
|
|
328
|
-
| `MESSENGER_AGENT_NAME` | — | Nom
|
|
368
|
+
| `MESSENGER_AGENT_NAME` | — | Nom de l'agent (le projet, typiquement), affiché à côté du nom de la boîte |
|
|
329
369
|
| `MESSENGER_AGENT_DESCRIPTION` | — | À quoi sert cet agent, pour ses correspondants |
|
|
330
370
|
| `MESSENGER_TIMEOUT_MS` | `15000` | Délai maximal d'un appel HTTP |
|
|
331
371
|
| `MESSENGER_STATE_DIR` | — | Répertoire des relais vers le veilleur (défaut : `~/.mcp-messenger`) |
|
|
@@ -344,7 +384,7 @@ la console (cookie), qui elle n'ouvre rien d'autre.
|
|
|
344
384
|
|---|---|---|
|
|
345
385
|
| `GET` | `/health` | État du service |
|
|
346
386
|
| `POST` | `/api/agents/register` | S'inscrire à l'annuaire (`fresh: true` = exige une adresse neuve) |
|
|
347
|
-
| `GET` | `/api/agents` | Annuaire des boîtes |
|
|
387
|
+
| `GET` | `/api/agents` | Annuaire des boîtes, sauf celle de l'appelant |
|
|
348
388
|
| `GET` | `/api/agents/:id` | Une boîte, ou 404 |
|
|
349
389
|
| `PATCH` | `/api/agents/me` | Renommer la boîte courante |
|
|
350
390
|
| `GET` | `/api/mailbox` | Compteurs de la boîte, et présence d'un veilleur (`watcher`) |
|
|
@@ -44,7 +44,7 @@ const REQUIRED_VARS = [
|
|
|
44
44
|
];
|
|
45
45
|
const OPTIONAL_VARS = [
|
|
46
46
|
["MESSENGER_AGENT_ID", "adresse fixe : à laisser vide pour une boîte par session"],
|
|
47
|
-
["MESSENGER_AGENT_NAME", "nom
|
|
47
|
+
["MESSENGER_AGENT_NAME", "nom de l'agent (le projet, typiquement) ; distinct du nom de la boîte, que l'agent pose lui-même"],
|
|
48
48
|
["MESSENGER_AGENT_DESCRIPTION", "à quoi sert cet agent, pour ses correspondants"],
|
|
49
49
|
["MESSENGER_TIMEOUT_MS", "délai maximal d'un appel HTTP (défaut : 15000)"],
|
|
50
50
|
];
|
package/dist/src/cli/watch.js
CHANGED
|
@@ -44,8 +44,8 @@ export function resolveWatchConfig(mailbox, env = process.env) {
|
|
|
44
44
|
agentId: target,
|
|
45
45
|
serverUrl: serverUrl.replace(/\/+$/, ""),
|
|
46
46
|
token: handoff?.token ?? env.MESSENGER_TOKEN?.trim() ?? null,
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
agentName: null,
|
|
48
|
+
agentDescription: null,
|
|
49
49
|
timeoutMs: Number(env.MESSENGER_TIMEOUT_MS) || 15_000,
|
|
50
50
|
},
|
|
51
51
|
};
|
|
@@ -52,7 +52,7 @@ export declare class MessengerClient {
|
|
|
52
52
|
register(options?: {
|
|
53
53
|
fresh?: boolean;
|
|
54
54
|
}): Promise<Agent>;
|
|
55
|
-
/** Renomme la boîte courante, pour
|
|
55
|
+
/** Renomme la boîte courante (pas l'agent), pour dire de quoi cette session s'occupe. */
|
|
56
56
|
updateProfile(profile: {
|
|
57
57
|
display_name?: string | null;
|
|
58
58
|
description?: string | null;
|
|
@@ -61,8 +61,7 @@ export declare class MessengerClient {
|
|
|
61
61
|
listAgents(): Promise<AgentDirectoryEntry[]>;
|
|
62
62
|
stats(): Promise<MailboxStats>;
|
|
63
63
|
sendMessage(input: {
|
|
64
|
-
to: string
|
|
65
|
-
cc?: string[];
|
|
64
|
+
to: string;
|
|
66
65
|
subject: string;
|
|
67
66
|
body: string;
|
|
68
67
|
priority?: Priority;
|
|
@@ -68,14 +68,14 @@ export class MessengerClient {
|
|
|
68
68
|
async register(options = {}) {
|
|
69
69
|
const { agent } = await this.request("POST", "/api/agents/register", {
|
|
70
70
|
body: {
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
agent_name: this.config.agentName,
|
|
72
|
+
agent_description: this.config.agentDescription,
|
|
73
73
|
fresh: options.fresh ?? false,
|
|
74
74
|
},
|
|
75
75
|
});
|
|
76
76
|
return agent;
|
|
77
77
|
}
|
|
78
|
-
/** Renomme la boîte courante, pour
|
|
78
|
+
/** Renomme la boîte courante (pas l'agent), pour dire de quoi cette session s'occupe. */
|
|
79
79
|
async updateProfile(profile) {
|
|
80
80
|
const { agent } = await this.request("PATCH", "/api/agents/me", { body: profile });
|
|
81
81
|
return agent;
|
package/dist/src/config.d.ts
CHANGED
|
@@ -27,8 +27,9 @@ export interface AgentConfig {
|
|
|
27
27
|
agentId: string | null;
|
|
28
28
|
serverUrl: string;
|
|
29
29
|
token: string | null;
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
/** Identité de l'agent, stable : d'où il écrit (le projet). Distincte du nom de la boîte. */
|
|
31
|
+
agentName: string | null;
|
|
32
|
+
agentDescription: string | null;
|
|
32
33
|
timeoutMs: number;
|
|
33
34
|
}
|
|
34
35
|
export declare function isLoopbackHost(host: string): boolean;
|
package/dist/src/config.js
CHANGED
|
@@ -74,8 +74,8 @@ export function loadAgentConfig(env = process.env) {
|
|
|
74
74
|
agentId,
|
|
75
75
|
serverUrl: rawUrl.replace(/\/+$/, ""),
|
|
76
76
|
token: nonEmpty(env.MESSENGER_TOKEN),
|
|
77
|
-
|
|
78
|
-
|
|
77
|
+
agentName: nonEmpty(env.MESSENGER_AGENT_NAME),
|
|
78
|
+
agentDescription: nonEmpty(env.MESSENGER_AGENT_DESCRIPTION),
|
|
79
79
|
timeoutMs: parseIntEnv(env.MESSENGER_TIMEOUT_MS, 15_000, "MESSENGER_TIMEOUT_MS"),
|
|
80
80
|
};
|
|
81
81
|
}
|
package/dist/src/core/store.d.ts
CHANGED
|
@@ -5,6 +5,15 @@ export declare class MessengerError extends Error {
|
|
|
5
5
|
readonly status: number;
|
|
6
6
|
constructor(message: string, status?: number);
|
|
7
7
|
}
|
|
8
|
+
export interface EnsureAgentOptions {
|
|
9
|
+
registered?: boolean;
|
|
10
|
+
/** Identité de l'agent : d'où il vient. Posée par la configuration. */
|
|
11
|
+
agent_name?: string | null;
|
|
12
|
+
agent_description?: string | null;
|
|
13
|
+
/** Nom de la boîte : de quoi cette session s'occupe. Posé par l'agent lui-même. */
|
|
14
|
+
display_name?: string | null;
|
|
15
|
+
description?: string | null;
|
|
16
|
+
}
|
|
8
17
|
export interface StoreEvents {
|
|
9
18
|
/** Émis pour chaque destinataire d'un message, y compris la copie « sent » de l'expéditeur. */
|
|
10
19
|
message: [{
|
|
@@ -26,16 +35,21 @@ export declare class MessageStore extends EventEmitter<StoreEvents> {
|
|
|
26
35
|
constructor(dbPath: string);
|
|
27
36
|
private migrate;
|
|
28
37
|
close(): void;
|
|
29
|
-
/**
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
38
|
+
/**
|
|
39
|
+
* Crée la fiche au besoin. `registered: false` = agent connu seulement comme destinataire.
|
|
40
|
+
* Chaque champ omis (ou null) laisse la valeur en place : l'enregistrement pose l'identité
|
|
41
|
+
* de l'agent sans toucher au nom de la boîte, et `rename_mailbox` fait l'inverse.
|
|
42
|
+
*/
|
|
43
|
+
ensureAgent(id: string, opts?: EnsureAgentOptions): Agent;
|
|
35
44
|
touchAgent(id: string): void;
|
|
36
45
|
getAgent(id: string): Agent | null;
|
|
37
|
-
/**
|
|
38
|
-
|
|
46
|
+
/**
|
|
47
|
+
* Annuaire classé par activité décroissante : ce qui bouge est en tête, l'id ne trie rien.
|
|
48
|
+
*
|
|
49
|
+
* `exclude` retire une adresse du résultat : l'annuaire servi à un agent ne lui montre pas
|
|
50
|
+
* sa propre boîte, la seule à laquelle il ne peut pas écrire.
|
|
51
|
+
*/
|
|
52
|
+
listAgents(exclude?: string): AgentDirectoryEntry[];
|
|
39
53
|
sendMessage(input: SendMessageInput): SendMessageResult;
|
|
40
54
|
listMessages(agentId: string, opts?: ListMessagesOptions): MessageSummary[];
|
|
41
55
|
getMessage(agentId: string, messageId: string, markRead?: boolean): Message;
|
|
@@ -103,6 +117,7 @@ export declare class MessageStore extends EventEmitter<StoreEvents> {
|
|
|
103
117
|
/** Filtre les ids fournis sur ceux réellement livrés à cet agent (isolation des boîtes). */
|
|
104
118
|
private ownedMessageIds;
|
|
105
119
|
private assertFolderExists;
|
|
120
|
+
/** Destinataire de chaque message demandé, par identifiant de message. */
|
|
106
121
|
private recipientsFor;
|
|
107
122
|
private toSummary;
|
|
108
123
|
private toMessage;
|
package/dist/src/core/store.js
CHANGED
|
@@ -52,9 +52,11 @@ export class MessageStore extends EventEmitter {
|
|
|
52
52
|
migrate() {
|
|
53
53
|
this.db.exec(`
|
|
54
54
|
CREATE TABLE IF NOT EXISTS agents (
|
|
55
|
-
id
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
id TEXT PRIMARY KEY,
|
|
56
|
+
agent_name TEXT,
|
|
57
|
+
agent_description TEXT,
|
|
58
|
+
display_name TEXT,
|
|
59
|
+
description TEXT,
|
|
58
60
|
created_at TEXT NOT NULL,
|
|
59
61
|
last_seen_at TEXT,
|
|
60
62
|
registered INTEGER NOT NULL DEFAULT 0
|
|
@@ -103,6 +105,21 @@ export class MessageStore extends EventEmitter {
|
|
|
103
105
|
CREATE INDEX IF NOT EXISTS idx_deliveries_message ON deliveries (message_id);
|
|
104
106
|
CREATE INDEX IF NOT EXISTS idx_messages_thread ON messages (thread_id, created_at);
|
|
105
107
|
`);
|
|
108
|
+
// Bases antérieures à la séparation agent / boîte : `display_name` y tenait les deux
|
|
109
|
+
// rôles, et venait presque toujours de la configuration (le nom du projet). On le
|
|
110
|
+
// déplace vers l'identité de l'agent ; la boîte repart sans nom, à poser par l'agent.
|
|
111
|
+
const agentColumns = this.db.prepare("PRAGMA table_info(agents)").all().map((c) => c.name);
|
|
112
|
+
if (!agentColumns.includes("agent_name")) {
|
|
113
|
+
this.db.exec(`
|
|
114
|
+
ALTER TABLE agents ADD COLUMN agent_name TEXT;
|
|
115
|
+
ALTER TABLE agents ADD COLUMN agent_description TEXT;
|
|
116
|
+
UPDATE agents
|
|
117
|
+
SET agent_name = display_name,
|
|
118
|
+
agent_description = description,
|
|
119
|
+
display_name = NULL,
|
|
120
|
+
description = NULL;
|
|
121
|
+
`);
|
|
122
|
+
}
|
|
106
123
|
// Bases antérieures aux fils nommés : chaque fil déjà présent reçoit sa ligne, sans
|
|
107
124
|
// titre. L'affichage retombe alors sur le sujet d'ouverture, comme avant.
|
|
108
125
|
this.db.exec(`
|
|
@@ -117,25 +134,31 @@ export class MessageStore extends EventEmitter {
|
|
|
117
134
|
this.db.close();
|
|
118
135
|
}
|
|
119
136
|
// --- Agents -------------------------------------------------------------
|
|
120
|
-
/**
|
|
137
|
+
/**
|
|
138
|
+
* Crée la fiche au besoin. `registered: false` = agent connu seulement comme destinataire.
|
|
139
|
+
* Chaque champ omis (ou null) laisse la valeur en place : l'enregistrement pose l'identité
|
|
140
|
+
* de l'agent sans toucher au nom de la boîte, et `rename_mailbox` fait l'inverse.
|
|
141
|
+
*/
|
|
121
142
|
ensureAgent(id, opts = {}) {
|
|
122
143
|
const now = new Date().toISOString();
|
|
123
144
|
const existing = this.db.prepare("SELECT id FROM agents WHERE id = ?").get(id);
|
|
124
145
|
if (!existing) {
|
|
125
146
|
this.db
|
|
126
|
-
.prepare(`INSERT INTO agents (id, display_name, description, created_at, last_seen_at, registered)
|
|
127
|
-
VALUES (?, ?, ?, ?, ?, ?)`)
|
|
128
|
-
.run(id, opts.display_name ?? null, opts.description ?? null, now, opts.registered ? now : null, opts.registered ? 1 : 0);
|
|
147
|
+
.prepare(`INSERT INTO agents (id, agent_name, agent_description, display_name, description, created_at, last_seen_at, registered)
|
|
148
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
149
|
+
.run(id, opts.agent_name ?? null, opts.agent_description ?? null, opts.display_name ?? null, opts.description ?? null, now, opts.registered ? now : null, opts.registered ? 1 : 0);
|
|
129
150
|
}
|
|
130
151
|
else {
|
|
131
152
|
this.db
|
|
132
153
|
.prepare(`UPDATE agents
|
|
133
|
-
SET
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
154
|
+
SET agent_name = COALESCE(?, agent_name),
|
|
155
|
+
agent_description = COALESCE(?, agent_description),
|
|
156
|
+
display_name = COALESCE(?, display_name),
|
|
157
|
+
description = COALESCE(?, description),
|
|
158
|
+
registered = MAX(registered, ?),
|
|
159
|
+
last_seen_at = CASE WHEN ? = 1 THEN ? ELSE last_seen_at END
|
|
137
160
|
WHERE id = ?`)
|
|
138
|
-
.run(opts.display_name ?? null, opts.description ?? null, opts.registered ? 1 : 0, opts.registered ? 1 : 0, now, id);
|
|
161
|
+
.run(opts.agent_name ?? null, opts.agent_description ?? null, opts.display_name ?? null, opts.description ?? null, opts.registered ? 1 : 0, opts.registered ? 1 : 0, now, id);
|
|
139
162
|
}
|
|
140
163
|
return this.getAgent(id);
|
|
141
164
|
}
|
|
@@ -146,13 +169,20 @@ export class MessageStore extends EventEmitter {
|
|
|
146
169
|
const row = this.db.prepare("SELECT * FROM agents WHERE id = ?").get(id);
|
|
147
170
|
return row ? { ...row, registered: Boolean(row.registered) } : null;
|
|
148
171
|
}
|
|
149
|
-
/**
|
|
150
|
-
|
|
172
|
+
/**
|
|
173
|
+
* Annuaire classé par activité décroissante : ce qui bouge est en tête, l'id ne trie rien.
|
|
174
|
+
*
|
|
175
|
+
* `exclude` retire une adresse du résultat : l'annuaire servi à un agent ne lui montre pas
|
|
176
|
+
* sa propre boîte, la seule à laquelle il ne peut pas écrire.
|
|
177
|
+
*/
|
|
178
|
+
listAgents(exclude) {
|
|
179
|
+
const excluded = exclude ? normalizeAgentId(exclude) : "";
|
|
151
180
|
const rows = this.db
|
|
152
181
|
.prepare(`SELECT a.*, ${LAST_ACTIVITY} AS last_activity_at
|
|
153
182
|
FROM agents a
|
|
183
|
+
WHERE a.id != ?
|
|
154
184
|
ORDER BY last_activity_at DESC, a.id`)
|
|
155
|
-
.all();
|
|
185
|
+
.all(excluded);
|
|
156
186
|
return rows.map((r) => ({
|
|
157
187
|
...r,
|
|
158
188
|
registered: Boolean(r.registered),
|
|
@@ -169,11 +199,14 @@ export class MessageStore extends EventEmitter {
|
|
|
169
199
|
const from = normalizeAgentId(input.from);
|
|
170
200
|
if (!from)
|
|
171
201
|
throw new MessengerError("sender agent id is required");
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
202
|
+
// Un message va d'une boîte à une seule autre : pas de diffusion, pas de copie.
|
|
203
|
+
const to = normalizeAgentId(input.to);
|
|
204
|
+
if (!to)
|
|
205
|
+
throw new MessengerError("a recipient is required in `to`");
|
|
206
|
+
// Une boîte ne se parle pas à elle-même : la copie « sent » et la copie « inbox »
|
|
207
|
+
// désigneraient le même agent, et le fil n'aurait qu'un seul participant.
|
|
208
|
+
if (to === from)
|
|
209
|
+
throw new MessengerError("an agent cannot send a message to itself");
|
|
177
210
|
const title = normalizeLabel(input.thread_title, THREAD_TITLE_MAX, "thread_title");
|
|
178
211
|
const threadDescription = normalizeLabel(input.thread_description, THREAD_DESCRIPTION_MAX, "thread_description");
|
|
179
212
|
let threadId;
|
|
@@ -191,18 +224,16 @@ export class MessageStore extends EventEmitter {
|
|
|
191
224
|
const messageId = newId("msg");
|
|
192
225
|
const now = new Date().toISOString();
|
|
193
226
|
const priority = input.priority ?? "normal";
|
|
194
|
-
const
|
|
227
|
+
const unknownRecipient = !this.getAgent(to)?.registered;
|
|
195
228
|
const deliveries = [
|
|
196
|
-
|
|
197
|
-
...cc.map((agent_id) => ({ agent_id, role: "cc", folder: "inbox", read_at: null })),
|
|
229
|
+
{ agent_id: to, role: "to", folder: "inbox", read_at: null },
|
|
198
230
|
{ agent_id: from, role: "sender", folder: "sent", read_at: now },
|
|
199
231
|
];
|
|
200
232
|
this.db.transaction(() => {
|
|
201
233
|
this.ensureAgent(from);
|
|
202
234
|
// Écrire est le signe d'activité le plus fiable : l'annuaire s'appuie dessus.
|
|
203
235
|
this.touchAgent(from);
|
|
204
|
-
|
|
205
|
-
this.ensureAgent(id);
|
|
236
|
+
this.ensureAgent(to);
|
|
206
237
|
// Le fil naît avec son premier message ; sur une réponse, la ligne existe déjà et
|
|
207
238
|
// n'est touchée que si l'expéditeur fournit un nouveau titre.
|
|
208
239
|
this.db
|
|
@@ -231,8 +262,8 @@ export class MessageStore extends EventEmitter {
|
|
|
231
262
|
thread_id: threadId,
|
|
232
263
|
thread_title: this.getThreadInfo(threadId)?.title ?? null,
|
|
233
264
|
created_at: now,
|
|
234
|
-
delivered_to:
|
|
235
|
-
|
|
265
|
+
delivered_to: to,
|
|
266
|
+
unknown_recipient: unknownRecipient,
|
|
236
267
|
};
|
|
237
268
|
}
|
|
238
269
|
// --- Lecture ------------------------------------------------------------
|
|
@@ -282,7 +313,7 @@ export class MessageStore extends EventEmitter {
|
|
|
282
313
|
JOIN messages m ON m.id = d.message_id
|
|
283
314
|
${THREAD_JOIN}
|
|
284
315
|
WHERE d.agent_id = ? AND d.message_id = ?
|
|
285
|
-
ORDER BY CASE d.role WHEN 'to' THEN 0
|
|
316
|
+
ORDER BY CASE d.role WHEN 'to' THEN 0 ELSE 1 END
|
|
286
317
|
LIMIT 1`)
|
|
287
318
|
.get(agentId, messageId);
|
|
288
319
|
if (!row)
|
|
@@ -628,6 +659,7 @@ export class MessageStore extends EventEmitter {
|
|
|
628
659
|
if (!row)
|
|
629
660
|
throw new MessengerError(`unknown folder: ${folder}`, 404);
|
|
630
661
|
}
|
|
662
|
+
/** Destinataire de chaque message demandé, par identifiant de message. */
|
|
631
663
|
recipientsFor(messageIds) {
|
|
632
664
|
const map = new Map();
|
|
633
665
|
const ids = dedupe(messageIds);
|
|
@@ -635,29 +667,20 @@ export class MessageStore extends EventEmitter {
|
|
|
635
667
|
return map;
|
|
636
668
|
const placeholders = ids.map(() => "?").join(",");
|
|
637
669
|
const rows = this.db
|
|
638
|
-
.prepare(`SELECT message_id, agent_id
|
|
639
|
-
WHERE message_id IN (${placeholders}) AND role
|
|
640
|
-
ORDER BY agent_id`)
|
|
670
|
+
.prepare(`SELECT message_id, agent_id FROM deliveries
|
|
671
|
+
WHERE message_id IN (${placeholders}) AND role = 'to'`)
|
|
641
672
|
.all(...ids);
|
|
642
|
-
for (const row of rows)
|
|
643
|
-
|
|
644
|
-
if (row.role === "to")
|
|
645
|
-
entry.to.push(row.agent_id);
|
|
646
|
-
else if (row.role === "cc")
|
|
647
|
-
entry.cc.push(row.agent_id);
|
|
648
|
-
map.set(row.message_id, entry);
|
|
649
|
-
}
|
|
673
|
+
for (const row of rows)
|
|
674
|
+
map.set(row.message_id, row.agent_id);
|
|
650
675
|
return map;
|
|
651
676
|
}
|
|
652
677
|
toSummary(row, recipients) {
|
|
653
|
-
const rcpt = recipients.get(row.message_id) ?? { to: [], cc: [] };
|
|
654
678
|
return {
|
|
655
679
|
message_id: row.message_id,
|
|
656
680
|
thread_id: row.thread_id,
|
|
657
681
|
thread_title: row.thread_title,
|
|
658
682
|
from: row.sender_id,
|
|
659
|
-
to:
|
|
660
|
-
cc: rcpt.cc,
|
|
683
|
+
to: recipients.get(row.message_id) ?? "",
|
|
661
684
|
subject: row.subject,
|
|
662
685
|
preview: preview(row.body),
|
|
663
686
|
priority: row.priority,
|
package/dist/src/core/types.d.ts
CHANGED
|
@@ -3,11 +3,22 @@ export declare const SYSTEM_FOLDERS: readonly ["inbox", "sent", "archive", "tras
|
|
|
3
3
|
export type SystemFolder = (typeof SYSTEM_FOLDERS)[number];
|
|
4
4
|
export declare const PRIORITIES: readonly ["low", "normal", "high"];
|
|
5
5
|
export type Priority = (typeof PRIORITIES)[number];
|
|
6
|
-
/** Rôle d'un agent vis-à-vis d'un message : destinataire
|
|
7
|
-
export declare const DELIVERY_ROLES: readonly ["to", "
|
|
6
|
+
/** Rôle d'un agent vis-à-vis d'un message : destinataire ou expéditeur. */
|
|
7
|
+
export declare const DELIVERY_ROLES: readonly ["to", "sender"];
|
|
8
8
|
export type DeliveryRole = (typeof DELIVERY_ROLES)[number];
|
|
9
|
+
/**
|
|
10
|
+
* Une boîte porte deux identités qui ne bougent pas au même rythme :
|
|
11
|
+
* - l'agent (`agent_name`, `agent_description`) : d'où il vient, typiquement le projet.
|
|
12
|
+
* Posé par la configuration à l'enregistrement, stable pour toute la vie de la boîte.
|
|
13
|
+
* - la boîte (`display_name`, `description`) : de quoi cette session s'occupe. Vide au
|
|
14
|
+
* départ, posé par l'agent lui-même (`rename_mailbox`), et libre de changer.
|
|
15
|
+
* Le nom de l'agent seul ne distingue pas deux sessions du même projet ; le nom de la boîte
|
|
16
|
+
* seul ne dit pas où l'agent travaille. La console et l'annuaire montrent les deux.
|
|
17
|
+
*/
|
|
9
18
|
export interface Agent {
|
|
10
19
|
id: string;
|
|
20
|
+
agent_name: string | null;
|
|
21
|
+
agent_description: string | null;
|
|
11
22
|
display_name: string | null;
|
|
12
23
|
description: string | null;
|
|
13
24
|
created_at: string;
|
|
@@ -26,8 +37,8 @@ export interface AgentDirectoryEntry extends Agent {
|
|
|
26
37
|
}
|
|
27
38
|
export interface SendMessageInput {
|
|
28
39
|
from: string;
|
|
29
|
-
|
|
30
|
-
|
|
40
|
+
/** Un échange lie exactement deux boîtes : un seul destinataire par message. */
|
|
41
|
+
to: string;
|
|
31
42
|
subject: string;
|
|
32
43
|
body: string;
|
|
33
44
|
priority?: Priority;
|
|
@@ -67,9 +78,9 @@ export interface SendMessageResult {
|
|
|
67
78
|
/** Titre du fil après envoi, null s'il n'a jamais été nommé. */
|
|
68
79
|
thread_title: string | null;
|
|
69
80
|
created_at: string;
|
|
70
|
-
delivered_to: string
|
|
71
|
-
/**
|
|
72
|
-
|
|
81
|
+
delivered_to: string;
|
|
82
|
+
/** Destinataire jamais enregistré sur ce serveur — le message l'attend malgré tout. */
|
|
83
|
+
unknown_recipient: boolean;
|
|
73
84
|
}
|
|
74
85
|
export interface MessageSummary {
|
|
75
86
|
message_id: string;
|
|
@@ -77,8 +88,7 @@ export interface MessageSummary {
|
|
|
77
88
|
/** Titre du fil, null tant que personne ne l'a nommé — le `subject` fait alors office. */
|
|
78
89
|
thread_title: string | null;
|
|
79
90
|
from: string;
|
|
80
|
-
to: string
|
|
81
|
-
cc: string[];
|
|
91
|
+
to: string;
|
|
82
92
|
subject: string;
|
|
83
93
|
preview: string;
|
|
84
94
|
priority: Priority;
|
package/dist/src/core/types.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Dossiers présents pour tout agent : ils ne peuvent être ni créés ni supprimés. */
|
|
2
2
|
export const SYSTEM_FOLDERS = ["inbox", "sent", "archive", "trash"];
|
|
3
3
|
export const PRIORITIES = ["low", "normal", "high"];
|
|
4
|
-
/** Rôle d'un agent vis-à-vis d'un message : destinataire
|
|
5
|
-
export const DELIVERY_ROLES = ["to", "
|
|
4
|
+
/** Rôle d'un agent vis-à-vis d'un message : destinataire ou expéditeur. */
|
|
5
|
+
export const DELIVERY_ROLES = ["to", "sender"];
|
package/dist/src/mcp/start.js
CHANGED
|
@@ -8,9 +8,29 @@ import { newId } from "../core/ids.js";
|
|
|
8
8
|
import { logger } from "../logger.js";
|
|
9
9
|
import { APP_VERSION } from "../version.js";
|
|
10
10
|
import { TOOLS, callTool } from "./tools.js";
|
|
11
|
+
/**
|
|
12
|
+
* Ce que l'hôte reçoit à la poignée de main, avant le moindre appel d'outil.
|
|
13
|
+
*
|
|
14
|
+
* Deux confusions coûtent cher et n'apparaissent qu'au moment d'écrire, trop tard : croire
|
|
15
|
+
* que le nom d'agent désigne une session (il est partagé par toutes celles du projet), et
|
|
16
|
+
* recopier une adresse dans le corps d'un message (le serveur la pose déjà, la copie ment
|
|
17
|
+
* dès qu'elle vieillit). Les dire ici, c'est les dire avant l'erreur.
|
|
18
|
+
*/
|
|
19
|
+
const INSTRUCTIONS = `mcp-messenger gives this session a mailbox and lets it exchange messages with other agent sessions.
|
|
20
|
+
|
|
21
|
+
Who you are, and who the others are — read this before writing to anyone:
|
|
22
|
+
- A session is identified by its mailbox address, and by nothing else. \`whoami\` gives you yours.
|
|
23
|
+
- The agent name (e.g. "Ai Corrector Dev") comes from the configuration and says which project the agent works on. It is NOT unique: every session started from that configuration carries it.
|
|
24
|
+
- So several mailboxes in \`list_agents\` can carry the same agent name as yours. Recognising your own agent name there does not mean you are looking at yourself — it is another session of the same project, a different agent, with its own mailbox. Compare addresses, never names.
|
|
25
|
+
- Your own mailbox is never listed in \`list_agents\`: every address you see there belongs to someone else.
|
|
26
|
+
|
|
27
|
+
Writing:
|
|
28
|
+
- One message goes to exactly one agent, and never to yourself. To reach several agents, send each one its own message.
|
|
29
|
+
- Never write a mailbox address in the body of a message. The server attaches the sender and the recipient to every message, and the reader sees them: an address copied by hand adds nothing and misleads as soon as it is stale or mistaken.
|
|
30
|
+
- Call \`enable_notifications\` once at the start of the session and arm the watcher it hands you. Until then, no incoming message reaches you.`;
|
|
11
31
|
/** Serveur MCP prêt à être branché sur n'importe quel transport (stdio en production, mémoire en test). */
|
|
12
32
|
export function createMcpServer(client) {
|
|
13
|
-
const server = new Server({ name: "mcp-messenger", version: APP_VERSION }, { capabilities: { tools: {} } });
|
|
33
|
+
const server = new Server({ name: "mcp-messenger", version: APP_VERSION }, { capabilities: { tools: {} }, instructions: INSTRUCTIONS });
|
|
14
34
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
|
|
15
35
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
16
36
|
const { name, arguments: args } = request.params;
|