@imenam/mcp-messenger 1.0.6 → 1.0.7

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,23 @@ 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.",
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
82
  body: { type: "string", description: "Message body, plain text or markdown." },
76
83
  priority: { type: "string", enum: [...PRIORITIES], description: "Defaults to normal." },
77
84
  reply_to: { type: "string", description: "Id of the message being answered; the reply joins its thread." },
78
- thread_title: {
85
+ thread_name: {
79
86
  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.",
87
+ 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
88
  },
82
89
  thread_description: { type: "string", description: "Optional longer description of the thread." },
83
90
  },
@@ -210,13 +217,12 @@ export const TOOLS = [
210
217
  ];
211
218
  const schemas = {
212
219
  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(),
220
+ to: z.string().min(1),
215
221
  subject: z.string().min(1),
216
222
  body: z.string().min(1),
217
223
  priority: z.enum(PRIORITIES).optional(),
218
224
  reply_to: z.string().optional(),
219
- thread_title: z.string().trim().min(1).max(200).optional(),
225
+ thread_name: z.string().trim().min(1).max(200).optional(),
220
226
  thread_description: z.string().trim().min(1).max(2000).optional(),
221
227
  }),
222
228
  list_messages: z.object({
@@ -238,6 +244,24 @@ const schemas = {
238
244
  move_messages: z.object({ message_ids: z.array(z.string().min(1)).min(1), folder: z.string().min(1) }),
239
245
  delete_messages: z.object({ message_ids: z.array(z.string().min(1)).min(1), permanent: z.boolean().optional() }),
240
246
  attach_to_mailbox: z.object({ mailbox_id: z.string().trim().min(1).optional() }),
247
+ rename_mailbox: z
248
+ .object({
249
+ name: z.string().trim().min(1).max(200).optional(),
250
+ description: z.string().trim().min(1).max(2000).optional(),
251
+ })
252
+ .refine((v) => v.name !== undefined || v.description !== undefined, {
253
+ message: "provide `name` and/or `description`",
254
+ }),
255
+ rename_thread: z
256
+ .object({
257
+ thread_id: z.string().trim().min(1),
258
+ name: z.string().trim().min(1).max(200).optional(),
259
+ description: z.string().trim().min(1).max(2000).optional(),
260
+ })
261
+ .refine((v) => v.name !== undefined || v.description !== undefined, {
262
+ message: "provide `name` and/or `description`",
263
+ }),
264
+ // Ancien outil `set_label` : une seule entrée pour la boîte et le fil, `label` au lieu de `name`.
241
265
  set_label: z
242
266
  .object({
243
267
  label: z.string().trim().min(1).max(200).optional(),
@@ -273,9 +297,10 @@ export async function callTool(client, name, rawArgs) {
273
297
  const lines = [
274
298
  `Mailbox address: ${client.agentId}`,
275
299
  " ↳ give this address to another agent so it can write to you.",
300
+ `Agent: ${self?.agent_name ?? "unnamed (set MESSENGER_AGENT_NAME in the MCP configuration)"}`,
276
301
  self?.display_name
277
302
  ? `Mailbox name: ${self.display_name}`
278
- : "Mailbox name: none — call set_label so the user can tell this session apart from the others.",
303
+ : "Mailbox name: none — call rename_mailbox so the user can tell this session apart from the others.",
279
304
  ];
280
305
  if (client.isAttachedElsewhere) {
281
306
  lines.push(`Attached to another mailbox. This session's own mailbox is ${client.sessionAgentId}.`);
@@ -303,8 +328,9 @@ export async function callTool(client, name, rawArgs) {
303
328
  client.setAgentId(target.id);
304
329
  const attachedStats = await client.stats();
305
330
  return [
306
- `Attached to mailbox ${target.id}${target.display_name ? ` (${target.display_name})` : ""}.`,
307
- target.description ? `Description: ${target.description}` : null,
331
+ `Attached to mailbox ${target.id}${formatAgentNames(target) ? ` (${formatAgentNames(target)})` : ""}.`,
332
+ target.agent_description ? `Agent: ${target.agent_description}` : null,
333
+ target.description ? `Mailbox: ${target.description}` : null,
308
334
  `Unread (excluding trash): ${attachedStats.unread_total}`,
309
335
  client.sessionAgentId && client.sessionAgentId !== target.id
310
336
  ? `This session's own mailbox remains ${client.sessionAgentId} — attach_to_mailbox with no argument goes back to it.`
@@ -313,29 +339,24 @@ export async function callTool(client, name, rawArgs) {
313
339
  .filter(Boolean)
314
340
  .join("\n");
315
341
  }
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.
342
+ case "rename_mailbox": {
343
+ const input = schemas.rename_mailbox.parse(args);
344
+ return renameMailbox(client, input.name ?? null, input.description ?? null);
345
+ }
346
+ case "rename_thread": {
347
+ const input = schemas.rename_thread.parse(args);
348
+ return renameThread(client, input.thread_id, input.name ?? null, input.description ?? null);
349
+ }
350
+ // `set_label` et `set_mailbox_label` ne sont plus publiés, mais restent acceptés : une
351
+ // session en cours peut les avoir appris avant la mise à jour du serveur. Le mot « label »
352
+ // faisait penser à une étiquette, alors qu'il s'agissait du nom.
318
353
  case "set_mailbox_label":
319
354
  case "set_label": {
320
355
  const input = schemas.set_label.parse(args);
321
356
  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");
357
+ return renameThread(client, input.thread_id, input.label ?? null, input.description ?? null);
333
358
  }
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}".`;
359
+ return renameMailbox(client, input.label ?? null, input.description ?? null);
339
360
  }
340
361
  case "list_threads": {
341
362
  const input = schemas.list_threads.parse(args);
@@ -347,41 +368,44 @@ export async function callTool(client, name, rawArgs) {
347
368
  case "list_agents": {
348
369
  const agents = await client.listAgents();
349
370
  if (agents.length === 0)
350
- return "No agent registered yet on this server.";
371
+ return "No other agent on this server yet.";
351
372
  return agents
352
373
  .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;
374
+ const flags = agent.registered ? "" : "never connected";
375
+ const names = formatAgentNames(agent);
376
+ const title = names ? `${agent.id} — ${names}` : agent.id;
357
377
  // L'annuaire est classé par cette date : l'afficher rend l'ordre lisible.
358
378
  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");
379
+ return [
380
+ `• ${title}${flags ? ` (${flags})` : ""}`,
381
+ ` ${agent.agent_description ?? agent.description ?? "no description"}`,
382
+ ` ${seen}`,
383
+ ].join("\n");
360
384
  })
361
385
  .join("\n");
362
386
  }
363
387
  case "send_message": {
364
388
  const input = schemas.send_message.parse(args);
365
389
  const result = await client.sendMessage({
366
- to: asArray(input.to),
367
- cc: asArray(input.cc),
390
+ to: input.to,
368
391
  subject: input.subject,
369
392
  body: input.body,
370
393
  priority: input.priority,
371
394
  reply_to: input.reply_to ?? null,
372
- thread_title: input.thread_title ?? null,
395
+ // Le protocole HTTP et le stockage gardent `thread_title` ; seul l'outil dit `thread_name`.
396
+ thread_title: input.thread_name ?? null,
373
397
  thread_description: input.thread_description ?? null,
374
398
  });
375
399
  const lines = [
376
400
  `Message sent: ${result.message_id}`,
377
401
  `Thread: ${formatThreadRef(result.thread_title, result.thread_id)}`,
378
- `Delivered to: ${result.delivered_to.join(", ")}`,
402
+ `Delivered to: ${result.delivered_to}`,
379
403
  ];
380
404
  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.");
405
+ lines.push("This thread has no name yet — rename_thread with its thread_id makes it readable for everyone.");
382
406
  }
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(", ")}`);
407
+ if (result.unknown_recipient) {
408
+ lines.push(`Warning — ${result.delivered_to} has never connected to this server, the message will wait in their mailbox.`);
385
409
  }
386
410
  return lines.join("\n");
387
411
  }
@@ -497,10 +521,28 @@ export async function callTool(client, name, rawArgs) {
497
521
  throw new Error(`Unknown tool: ${name}`);
498
522
  }
499
523
  }
500
- function asArray(value) {
501
- if (value === undefined)
502
- return [];
503
- return Array.isArray(value) ? value : [value];
524
+ async function renameMailbox(client, name, description) {
525
+ const agent = await client.updateProfile({ display_name: name, description });
526
+ return [
527
+ `Mailbox ${agent.id} named "${agent.display_name}".`,
528
+ agent.agent_name ? `The agent name stays "${agent.agent_name}"; both are shown together.` : null,
529
+ ]
530
+ .filter(Boolean)
531
+ .join("\n");
532
+ }
533
+ /** `Agent · Boîte` : l'agent dit d'où l'on écrit, la boîte de quoi il est question. */
534
+ function formatAgentNames(agent) {
535
+ return [agent.agent_name, agent.display_name].filter(Boolean).join(" · ");
536
+ }
537
+ async function renameThread(client, threadId, name, description) {
538
+ const thread = await client.setThreadLabel(threadId, { title: name, description });
539
+ return [
540
+ `Thread ${thread.thread_id} is now named "${thread.title}".`,
541
+ thread.description ? `Description: ${thread.description}` : null,
542
+ "Every participant of the thread sees this name.",
543
+ ]
544
+ .filter(Boolean)
545
+ .join("\n");
504
546
  }
505
547
  /** `titre (id)`, ou l'id seul tant que le fil n'a pas été nommé. */
506
548
  function formatThreadRef(title, threadId) {
@@ -523,7 +565,7 @@ function formatSummary(message) {
523
565
  const priority = message.priority === "normal" ? "" : ` [${message.priority}]`;
524
566
  return [
525
567
  `${flags} ${message.subject}${priority}`,
526
- ` from ${message.from} to ${message.to.join(", ") || "—"}${message.cc.length ? ` (cc ${message.cc.join(", ")})` : ""} · ${message.created_at}`,
568
+ ` from ${message.from} to ${message.to || "—"} · ${message.created_at}`,
527
569
  ` ${message.preview}`,
528
570
  ` id ${message.message_id} · thread ${formatThreadRef(message.thread_title, message.thread_id)}`,
529
571
  ].join("\n");
@@ -532,8 +574,7 @@ function formatMessage(message) {
532
574
  const header = [
533
575
  `Subject: ${message.subject}`,
534
576
  `From: ${message.from}`,
535
- `To: ${message.to.join(", ") || "—"}`,
536
- message.cc.length ? `Cc: ${message.cc.join(", ")}` : null,
577
+ `To: ${message.to || "—"}`,
537
578
  `Date: ${message.created_at}`,
538
579
  `Priority: ${message.priority}`,
539
580
  `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
  *