@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.
@@ -25,24 +25,37 @@ export const TOOLS = [
25
25
  },
26
26
  },
27
27
  {
28
- name: "set_label",
29
- description: "Give a plain-language name to this mailbox, or to a thread. Ids are opaque: a name is what makes a mailbox or a conversation recognisable later, both for the other agents and for the user reading the web console. Without thread_id it names this mailbox ('Développement des tests unitaires du parser'); with thread_id it names that thread ('Le test échoue sur un bug applicatif'). A thread has a single shared name: renaming it changes it for every participant, and the last caller wins.",
28
+ name: "rename_mailbox",
29
+ description: "Give a plain-language name to this mailbox, or change it: what this session is working on, e.g. 'Développement des tests unitaires du parser'. Addresses are opaque: the name is what makes this session recognisable later, both for the other agents (list_agents) and for the user reading the web console. It does not touch the agent name, which says where the agent comes from (its project) and is set by the configuration.",
30
+ inputSchema: {
31
+ type: "object",
32
+ properties: {
33
+ name: { type: "string", description: "Short name, one line. Say what the work is about, not who does it." },
34
+ description: { type: "string", description: "Optional longer explanation of what this mailbox is for." },
35
+ },
36
+ additionalProperties: false,
37
+ },
38
+ },
39
+ {
40
+ name: "rename_thread",
41
+ description: "Give a plain-language name to a thread, or change it, e.g. 'Le test échoue sur un bug applicatif'. A thread has a single shared name: renaming it changes it for every participant, and the last caller wins. Use it when a thread was opened without a name, or when the discussion has drifted away from its original name.",
30
42
  inputSchema: {
31
43
  type: "object",
32
44
  properties: {
33
- label: { type: "string", description: "Short name, one line. Say what the work is about, not who does it." },
34
- description: { type: "string", description: "Optional longer explanation of the mailbox or the thread." },
35
45
  thread_id: {
36
46
  type: "string",
37
- description: "Thread to name. Omit to name this mailbox instead. You must be a participant of the thread.",
47
+ description: "Thread to rename. You must be a participant of the thread.",
38
48
  },
49
+ name: { type: "string", description: "Short name, one line. Say what the thread is about." },
50
+ description: { type: "string", description: "Optional longer explanation of the thread." },
39
51
  },
52
+ required: ["thread_id"],
40
53
  additionalProperties: false,
41
54
  },
42
55
  },
43
56
  {
44
57
  name: "list_threads",
45
- description: "List the conversations visible from this mailbox, most recently active first, with their name, participants and unread count. This is the readable map of the ongoing work: prefer it over list_messages when you want to know what is being discussed rather than what arrived last. A thread with no name falls back to the subject of its opening message — name it with set_label.",
58
+ description: "List the conversations visible from this mailbox, most recently active first, with their name, participants and unread count. This is the readable map of the ongoing work: prefer it over list_messages when you want to know what is being discussed rather than what arrived last. A thread with no name falls back to the subject of its opening message — name it with rename_thread.",
46
59
  inputSchema: {
47
60
  type: "object",
48
61
  properties: {
@@ -55,29 +68,26 @@ export const TOOLS = [
55
68
  },
56
69
  {
57
70
  name: "list_agents",
58
- description: "List the agents known to the mailbox server, most recently active first, with their name and description. Use it to find the address of the agent you want to write to.",
71
+ description: "List the other agents known to the mailbox server, most recently active first, with their name and description. Your own mailbox is never listed — you cannot write to yourself. Use it to find the address of the agent you want to write to.",
59
72
  inputSchema: { type: "object", properties: {}, additionalProperties: false },
60
73
  },
61
74
  {
62
75
  name: "send_message",
63
- description: "Send a message to one or more agents, identified by their mailbox address. Set reply_to to answer an existing message and keep the thread together.",
76
+ description: "Send a message to one agent, identified by its mailbox address. A message has a single recipient: to reach several agents, send each one its own message. Set reply_to to answer an existing message and keep the thread together. Never write a mailbox address in the body: the server attaches the sender and the recipient to every message, so an address copied by hand adds nothing and misleads as soon as it is wrong.",
64
77
  inputSchema: {
65
78
  type: "object",
66
79
  properties: {
67
- to: {
68
- type: "array",
69
- items: { type: "string" },
70
- minItems: 1,
71
- description: "Recipient agent ids (mailbox addresses).",
72
- },
73
- cc: { type: "array", items: { type: "string" }, description: "Agents to keep in copy." },
80
+ to: { type: "string", description: "Recipient agent id (mailbox address). Exactly one recipient." },
74
81
  subject: { type: "string", description: "Short subject line." },
75
- body: { type: "string", description: "Message body, plain text or markdown." },
82
+ body: {
83
+ type: "string",
84
+ description: "Message body, plain text or markdown. Do not restate your own address or the recipient's here.",
85
+ },
76
86
  priority: { type: "string", enum: [...PRIORITIES], description: "Defaults to normal." },
77
87
  reply_to: { type: "string", description: "Id of the message being answered; the reply joins its thread." },
78
- thread_title: {
88
+ thread_name: {
79
89
  type: "string",
80
- description: "Plain-language name of the conversation, e.g. 'Résolution de l'échec du test de parsing'. Name the thread when you open it — you already know what it is about. On a reply it renames the thread for everyone, which is what you want when the discussion has drifted.",
90
+ description: "Plain-language name of the conversation, e.g. 'Résolution de l'échec du test de parsing'. Name the thread when you open it — you already know what it is about. On a reply it renames the thread for everyone (same effect as rename_thread).",
81
91
  },
82
92
  thread_description: { type: "string", description: "Optional longer description of the thread." },
83
93
  },
@@ -210,13 +220,12 @@ export const TOOLS = [
210
220
  ];
211
221
  const schemas = {
212
222
  send_message: z.object({
213
- to: z.union([z.string(), z.array(z.string()).min(1)]),
214
- cc: z.union([z.string(), z.array(z.string())]).optional(),
223
+ to: z.string().min(1),
215
224
  subject: z.string().min(1),
216
225
  body: z.string().min(1),
217
226
  priority: z.enum(PRIORITIES).optional(),
218
227
  reply_to: z.string().optional(),
219
- thread_title: z.string().trim().min(1).max(200).optional(),
228
+ thread_name: z.string().trim().min(1).max(200).optional(),
220
229
  thread_description: z.string().trim().min(1).max(2000).optional(),
221
230
  }),
222
231
  list_messages: z.object({
@@ -238,6 +247,24 @@ const schemas = {
238
247
  move_messages: z.object({ message_ids: z.array(z.string().min(1)).min(1), folder: z.string().min(1) }),
239
248
  delete_messages: z.object({ message_ids: z.array(z.string().min(1)).min(1), permanent: z.boolean().optional() }),
240
249
  attach_to_mailbox: z.object({ mailbox_id: z.string().trim().min(1).optional() }),
250
+ rename_mailbox: z
251
+ .object({
252
+ name: z.string().trim().min(1).max(200).optional(),
253
+ description: z.string().trim().min(1).max(2000).optional(),
254
+ })
255
+ .refine((v) => v.name !== undefined || v.description !== undefined, {
256
+ message: "provide `name` and/or `description`",
257
+ }),
258
+ rename_thread: z
259
+ .object({
260
+ thread_id: z.string().trim().min(1),
261
+ name: z.string().trim().min(1).max(200).optional(),
262
+ description: z.string().trim().min(1).max(2000).optional(),
263
+ })
264
+ .refine((v) => v.name !== undefined || v.description !== undefined, {
265
+ message: "provide `name` and/or `description`",
266
+ }),
267
+ // Ancien outil `set_label` : une seule entrée pour la boîte et le fil, `label` au lieu de `name`.
241
268
  set_label: z
242
269
  .object({
243
270
  label: z.string().trim().min(1).max(200).optional(),
@@ -273,9 +300,15 @@ export async function callTool(client, name, rawArgs) {
273
300
  const lines = [
274
301
  `Mailbox address: ${client.agentId}`,
275
302
  " ↳ give this address to another agent so it can write to you.",
303
+ self?.agent_name
304
+ ? `Agent: ${self.agent_name}`
305
+ : "Agent: unnamed (set MESSENGER_AGENT_NAME in the MCP configuration)",
306
+ // Le nom vient de la configuration : toutes les sessions du projet le portent. Le
307
+ // dire ici évite qu'un agent prenne une session homonyme pour la sienne.
308
+ " ↳ this name is shared with every session of the same project; only the address above identifies you.",
276
309
  self?.display_name
277
310
  ? `Mailbox name: ${self.display_name}`
278
- : "Mailbox name: none — call set_label so the user can tell this session apart from the others.",
311
+ : "Mailbox name: none — call rename_mailbox so the user can tell this session apart from the others.",
279
312
  ];
280
313
  if (client.isAttachedElsewhere) {
281
314
  lines.push(`Attached to another mailbox. This session's own mailbox is ${client.sessionAgentId}.`);
@@ -303,8 +336,9 @@ export async function callTool(client, name, rawArgs) {
303
336
  client.setAgentId(target.id);
304
337
  const attachedStats = await client.stats();
305
338
  return [
306
- `Attached to mailbox ${target.id}${target.display_name ? ` (${target.display_name})` : ""}.`,
307
- target.description ? `Description: ${target.description}` : null,
339
+ `Attached to mailbox ${target.id}${formatAgentNames(target) ? ` (${formatAgentNames(target)})` : ""}.`,
340
+ target.agent_description ? `Agent: ${target.agent_description}` : null,
341
+ target.description ? `Mailbox: ${target.description}` : null,
308
342
  `Unread (excluding trash): ${attachedStats.unread_total}`,
309
343
  client.sessionAgentId && client.sessionAgentId !== target.id
310
344
  ? `This session's own mailbox remains ${client.sessionAgentId} — attach_to_mailbox with no argument goes back to it.`
@@ -313,29 +347,24 @@ export async function callTool(client, name, rawArgs) {
313
347
  .filter(Boolean)
314
348
  .join("\n");
315
349
  }
316
- // `set_mailbox_label` n'est plus publié, mais reste accepté : une session en cours
317
- // peut l'avoir appris avant la mise à jour du serveur.
350
+ case "rename_mailbox": {
351
+ const input = schemas.rename_mailbox.parse(args);
352
+ return renameMailbox(client, input.name ?? null, input.description ?? null);
353
+ }
354
+ case "rename_thread": {
355
+ const input = schemas.rename_thread.parse(args);
356
+ return renameThread(client, input.thread_id, input.name ?? null, input.description ?? null);
357
+ }
358
+ // `set_label` et `set_mailbox_label` ne sont plus publiés, mais restent acceptés : une
359
+ // session en cours peut les avoir appris avant la mise à jour du serveur. Le mot « label »
360
+ // faisait penser à une étiquette, alors qu'il s'agissait du nom.
318
361
  case "set_mailbox_label":
319
362
  case "set_label": {
320
363
  const input = schemas.set_label.parse(args);
321
364
  if (input.thread_id) {
322
- const thread = await client.setThreadLabel(input.thread_id, {
323
- title: input.label ?? null,
324
- description: input.description ?? null,
325
- });
326
- return [
327
- `Thread ${thread.thread_id} is now named "${thread.title}".`,
328
- thread.description ? `Description: ${thread.description}` : null,
329
- "Every participant of the thread sees this name.",
330
- ]
331
- .filter(Boolean)
332
- .join("\n");
365
+ return renameThread(client, input.thread_id, input.label ?? null, input.description ?? null);
333
366
  }
334
- const agent = await client.updateProfile({
335
- display_name: input.label ?? null,
336
- description: input.description ?? null,
337
- });
338
- return `Mailbox ${agent.id} named "${agent.display_name}".`;
367
+ return renameMailbox(client, input.label ?? null, input.description ?? null);
339
368
  }
340
369
  case "list_threads": {
341
370
  const input = schemas.list_threads.parse(args);
@@ -347,41 +376,54 @@ export async function callTool(client, name, rawArgs) {
347
376
  case "list_agents": {
348
377
  const agents = await client.listAgents();
349
378
  if (agents.length === 0)
350
- return "No agent registered yet on this server.";
351
- return agents
379
+ return "No other agent on this server yet.";
380
+ const directory = agents
352
381
  .map((agent) => {
353
- const flags = [agent.id === client.agentId ? "you" : null, agent.registered ? null : "never connected"]
354
- .filter(Boolean)
355
- .join(", ");
356
- const title = agent.display_name ? `${agent.id} — ${agent.display_name}` : agent.id;
382
+ const flags = agent.registered ? "" : "never connected";
383
+ const names = formatAgentNames(agent);
384
+ const title = names ? `${agent.id} — ${names}` : agent.id;
357
385
  // L'annuaire est classé par cette date : l'afficher rend l'ordre lisible.
358
386
  const seen = agent.last_activity_at ? `last active ${agent.last_activity_at}` : "never active";
359
- return [`• ${title}${flags ? ` (${flags})` : ""}`, ` ${agent.description ?? "no description"}`, ` ${seen}`].join("\n");
387
+ return [
388
+ `• ${title}${flags ? ` (${flags})` : ""}`,
389
+ ` ${agent.agent_description ?? agent.description ?? "no description"}`,
390
+ ` ${seen}`,
391
+ ].join("\n");
360
392
  })
361
393
  .join("\n");
394
+ // Un nom d'agent est celui d'un projet, pas d'une session : plusieurs boîtes le
395
+ // portent, y compris celle de l'appelant. Sans ce rappel, un agent croise son propre
396
+ // nom d'agent dans cette liste et prend la boîte d'une autre session pour la sienne.
397
+ return [
398
+ directory,
399
+ "",
400
+ "Addresses identify sessions; agent names do not. Several mailboxes above may carry the same",
401
+ "agent name as yours — they are other sessions of the same project, never you: your own mailbox",
402
+ "is never listed here. Always write to an address taken from this list, never to a name.",
403
+ ].join("\n");
362
404
  }
363
405
  case "send_message": {
364
406
  const input = schemas.send_message.parse(args);
365
407
  const result = await client.sendMessage({
366
- to: asArray(input.to),
367
- cc: asArray(input.cc),
408
+ to: input.to,
368
409
  subject: input.subject,
369
410
  body: input.body,
370
411
  priority: input.priority,
371
412
  reply_to: input.reply_to ?? null,
372
- thread_title: input.thread_title ?? null,
413
+ // Le protocole HTTP et le stockage gardent `thread_title` ; seul l'outil dit `thread_name`.
414
+ thread_title: input.thread_name ?? null,
373
415
  thread_description: input.thread_description ?? null,
374
416
  });
375
417
  const lines = [
376
418
  `Message sent: ${result.message_id}`,
377
419
  `Thread: ${formatThreadRef(result.thread_title, result.thread_id)}`,
378
- `Delivered to: ${result.delivered_to.join(", ")}`,
420
+ `Delivered to: ${result.delivered_to}`,
379
421
  ];
380
422
  if (!result.thread_title && !input.reply_to) {
381
- lines.push("This thread has no name yet — set_label with its thread_id makes it readable for everyone.");
423
+ lines.push("This thread has no name yet — rename_thread with its thread_id makes it readable for everyone.");
382
424
  }
383
- if (result.unknown_recipients.length > 0) {
384
- lines.push(`Warning — never connected to this server, the message will wait in their mailbox: ${result.unknown_recipients.join(", ")}`);
425
+ if (result.unknown_recipient) {
426
+ lines.push(`Warning — ${result.delivered_to} has never connected to this server, the message will wait in their mailbox.`);
385
427
  }
386
428
  return lines.join("\n");
387
429
  }
@@ -497,10 +539,28 @@ export async function callTool(client, name, rawArgs) {
497
539
  throw new Error(`Unknown tool: ${name}`);
498
540
  }
499
541
  }
500
- function asArray(value) {
501
- if (value === undefined)
502
- return [];
503
- return Array.isArray(value) ? value : [value];
542
+ async function renameMailbox(client, name, description) {
543
+ const agent = await client.updateProfile({ display_name: name, description });
544
+ return [
545
+ `Mailbox ${agent.id} named "${agent.display_name}".`,
546
+ agent.agent_name ? `The agent name stays "${agent.agent_name}"; both are shown together.` : null,
547
+ ]
548
+ .filter(Boolean)
549
+ .join("\n");
550
+ }
551
+ /** `Agent · Boîte` : l'agent dit d'où l'on écrit, la boîte de quoi il est question. */
552
+ function formatAgentNames(agent) {
553
+ return [agent.agent_name, agent.display_name].filter(Boolean).join(" · ");
554
+ }
555
+ async function renameThread(client, threadId, name, description) {
556
+ const thread = await client.setThreadLabel(threadId, { title: name, description });
557
+ return [
558
+ `Thread ${thread.thread_id} is now named "${thread.title}".`,
559
+ thread.description ? `Description: ${thread.description}` : null,
560
+ "Every participant of the thread sees this name.",
561
+ ]
562
+ .filter(Boolean)
563
+ .join("\n");
504
564
  }
505
565
  /** `titre (id)`, ou l'id seul tant que le fil n'a pas été nommé. */
506
566
  function formatThreadRef(title, threadId) {
@@ -523,7 +583,7 @@ function formatSummary(message) {
523
583
  const priority = message.priority === "normal" ? "" : ` [${message.priority}]`;
524
584
  return [
525
585
  `${flags} ${message.subject}${priority}`,
526
- ` from ${message.from} to ${message.to.join(", ") || "—"}${message.cc.length ? ` (cc ${message.cc.join(", ")})` : ""} · ${message.created_at}`,
586
+ ` from ${message.from} to ${message.to || "—"} · ${message.created_at}`,
527
587
  ` ${message.preview}`,
528
588
  ` id ${message.message_id} · thread ${formatThreadRef(message.thread_title, message.thread_id)}`,
529
589
  ].join("\n");
@@ -532,8 +592,7 @@ function formatMessage(message) {
532
592
  const header = [
533
593
  `Subject: ${message.subject}`,
534
594
  `From: ${message.from}`,
535
- `To: ${message.to.join(", ") || "—"}`,
536
- message.cc.length ? `Cc: ${message.cc.join(", ")}` : null,
595
+ `To: ${message.to || "—"}`,
537
596
  `Date: ${message.created_at}`,
538
597
  `Priority: ${message.priority}`,
539
598
  `Folder: ${message.folder}${message.read ? "" : " (unread)"}${message.starred ? " ★" : ""}`,
@@ -14,15 +14,20 @@ import { logger } from "../logger.js";
14
14
  const UI_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), "ui");
15
15
  const agentIdSchema = z.string().trim().min(1).max(128);
16
16
  const idListSchema = z.array(z.string().trim().min(1)).min(1).max(200);
17
- const registerSchema = z.object({
17
+ /** Nom et description de la boîte : ce que l'agent pose lui-même sur sa session. */
18
+ const mailboxProfileSchema = z.object({
18
19
  display_name: z.string().trim().max(200).nullish(),
19
20
  description: z.string().trim().max(2000).nullish(),
21
+ });
22
+ const registerSchema = mailboxProfileSchema.extend({
23
+ /** Identité de l'agent (le projet, typiquement) : vient de la configuration, pas de l'agent. */
24
+ agent_name: z.string().trim().max(200).nullish(),
25
+ agent_description: z.string().trim().max(2000).nullish(),
20
26
  /** true = l'adresse doit être inconnue du serveur (allocation d'une boîte neuve). */
21
27
  fresh: z.boolean().optional(),
22
28
  });
23
29
  const sendSchema = z.object({
24
- to: z.union([agentIdSchema, z.array(agentIdSchema).min(1)]),
25
- cc: z.union([agentIdSchema, z.array(agentIdSchema)]).optional(),
30
+ to: agentIdSchema,
26
31
  subject: z.string().min(1).max(500),
27
32
  body: z.string().min(1).max(200_000),
28
33
  priority: z.enum(PRIORITIES).optional(),
@@ -191,16 +196,21 @@ export function createApp({ store, config }) {
191
196
  }
192
197
  const agent = store.ensureAgent(req.agentId, {
193
198
  registered: true,
199
+ agent_name: input.agent_name ?? null,
200
+ agent_description: input.agent_description ?? null,
201
+ // Un client antérieur à la séparation agent / boîte n'envoie que ces deux-là.
194
202
  display_name: input.display_name ?? null,
195
203
  description: input.description ?? null,
196
204
  });
197
205
  res.json({ agent });
198
206
  });
199
- api.get("/agents", (_req, res) => {
200
- res.json({ agents: store.listAgents() });
207
+ // L'agent ne se voit pas dans son propre annuaire : sa boîte est la seule interdite d'envoi.
208
+ api.get("/agents", (req, res) => {
209
+ res.json({ agents: store.listAgents(req.agentId) });
201
210
  });
211
+ // Ne touche qu'à la boîte : l'identité de l'agent ne se renomme pas depuis une session.
202
212
  api.patch("/agents/me", (req, res) => {
203
- const input = registerSchema.parse(req.body ?? {});
213
+ const input = mailboxProfileSchema.parse(req.body ?? {});
204
214
  res.json({
205
215
  agent: store.ensureAgent(req.agentId, {
206
216
  registered: true,
@@ -222,8 +232,7 @@ export function createApp({ store, config }) {
222
232
  const input = sendSchema.parse(req.body ?? {});
223
233
  const result = store.sendMessage({
224
234
  from: req.agentId,
225
- to: toArray(input.to),
226
- cc: toArray(input.cc),
235
+ to: input.to,
227
236
  subject: input.subject,
228
237
  body: input.body,
229
238
  priority: input.priority,
@@ -231,7 +240,7 @@ export function createApp({ store, config }) {
231
240
  thread_title: input.thread_title ?? null,
232
241
  thread_description: input.thread_description ?? null,
233
242
  });
234
- logger.info(`message ${result.message_id}: ${req.agentId} -> ${result.delivered_to.join(", ")}`);
243
+ logger.info(`message ${result.message_id}: ${req.agentId} -> ${result.delivered_to}`);
235
244
  res.status(201).json(result);
236
245
  });
237
246
  api.get("/messages", (req, res) => {
@@ -337,11 +346,6 @@ export function createApp({ store, config }) {
337
346
  });
338
347
  return app;
339
348
  }
340
- function toArray(value) {
341
- if (value === undefined)
342
- return [];
343
- return Array.isArray(value) ? value : [value];
344
- }
345
349
  /**
346
350
  * Exige un jeton connu du serveur sur toute requête entrante.
347
351
  *