@imenam/mcp-messenger 1.0.9 → 1.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -14
- package/dist/src/client/messenger-client.d.ts +1 -0
- package/dist/src/core/store.d.ts +26 -1
- package/dist/src/core/store.js +264 -146
- package/dist/src/core/types.d.ts +29 -0
- package/dist/src/mcp/start.js +7 -2
- package/dist/src/mcp/tools.js +150 -37
- package/dist/src/server/app.js +25 -2
- package/dist/src/server/ui/app.js +703 -320
- package/dist/src/server/ui/index.html +92 -27
- package/dist/src/server/ui/styles.css +1147 -328
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/package.json +1 -1
package/dist/src/mcp/tools.js
CHANGED
|
@@ -55,11 +55,15 @@ export const TOOLS = [
|
|
|
55
55
|
},
|
|
56
56
|
{
|
|
57
57
|
name: "list_threads",
|
|
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.",
|
|
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. Set with_mailbox to see everything exchanged with one correspondent. A thread with no name falls back to the subject of its opening message — name it with rename_thread.",
|
|
59
59
|
inputSchema: {
|
|
60
60
|
type: "object",
|
|
61
61
|
properties: {
|
|
62
62
|
unread_only: { type: "boolean", description: "Keep only threads with unread messages." },
|
|
63
|
+
with_mailbox: {
|
|
64
|
+
type: "string",
|
|
65
|
+
description: "A correspondent's mailbox address, as listed by list_agents: keep only the threads shared with that mailbox. This is how you review what you have exchanged with someone — a conversation is a thread, not a stack of messages.",
|
|
66
|
+
},
|
|
63
67
|
search: { type: "string", description: "Free text matched against thread name, description and subject." },
|
|
64
68
|
limit: { type: "number", description: "1-100, defaults to 25." },
|
|
65
69
|
},
|
|
@@ -68,16 +72,25 @@ export const TOOLS = [
|
|
|
68
72
|
},
|
|
69
73
|
{
|
|
70
74
|
name: "list_agents",
|
|
71
|
-
description: "List the
|
|
72
|
-
inputSchema: {
|
|
75
|
+
description: "List the mailboxes known to the server, grouped by agent name — one group per project, one line per session mailbox inside it, most recently active first. An agent name is shared by every session of the same project, so a group with several lines means several live sessions, each with its own mailbox address. Your own mailbox is never listed — you cannot write to yourself. Use it to find the mailbox address you want to write to; pass agent_name to expand a group whose sessions were not all listed.",
|
|
76
|
+
inputSchema: {
|
|
77
|
+
type: "object",
|
|
78
|
+
properties: {
|
|
79
|
+
agent_name: {
|
|
80
|
+
type: "string",
|
|
81
|
+
description: "Name of one agent (project) to expand: lists every one of its session mailboxes in full. Omit it to get the whole directory, grouped.",
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
additionalProperties: false,
|
|
85
|
+
},
|
|
73
86
|
},
|
|
74
87
|
{
|
|
75
88
|
name: "send_message",
|
|
76
|
-
description: "Send a message to one
|
|
89
|
+
description: "Send a message to one mailbox, identified by its address. A message has a single recipient: to reach several correspondents, 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.",
|
|
77
90
|
inputSchema: {
|
|
78
91
|
type: "object",
|
|
79
92
|
properties: {
|
|
80
|
-
to: { type: "string", description: "Recipient
|
|
93
|
+
to: { type: "string", description: "Recipient mailbox address, as listed by list_agents. Exactly one recipient." },
|
|
81
94
|
subject: { type: "string", description: "Short subject line." },
|
|
82
95
|
body: {
|
|
83
96
|
type: "string",
|
|
@@ -97,13 +110,13 @@ export const TOOLS = [
|
|
|
97
110
|
},
|
|
98
111
|
{
|
|
99
112
|
name: "list_messages",
|
|
100
|
-
description: "List messages in this
|
|
113
|
+
description: "List messages in this mailbox, newest first, with a short preview. Filter by folder, unread state, sender, thread or free text.",
|
|
101
114
|
inputSchema: {
|
|
102
115
|
type: "object",
|
|
103
116
|
properties: {
|
|
104
117
|
folder: { type: "string", description: FOLDER_HINT + " Defaults to inbox." },
|
|
105
118
|
unread_only: { type: "boolean", description: "Keep only unread messages." },
|
|
106
|
-
from: { type: "string", description: "Only messages sent by this
|
|
119
|
+
from: { type: "string", description: "Only messages sent by this mailbox address." },
|
|
107
120
|
thread_id: { type: "string", description: "Only messages of this thread." },
|
|
108
121
|
search: { type: "string", description: "Free text matched against subject and body." },
|
|
109
122
|
limit: { type: "number", description: "1-100, defaults to 25." },
|
|
@@ -127,7 +140,7 @@ export const TOOLS = [
|
|
|
127
140
|
},
|
|
128
141
|
{
|
|
129
142
|
name: "get_thread",
|
|
130
|
-
description: "Read every message of a thread visible to this
|
|
143
|
+
description: "Read every message of a thread visible to this mailbox, oldest first — the full conversation.",
|
|
131
144
|
inputSchema: {
|
|
132
145
|
type: "object",
|
|
133
146
|
properties: { thread_id: { type: "string", description: "Thread id shown on any message." } },
|
|
@@ -164,7 +177,7 @@ export const TOOLS = [
|
|
|
164
177
|
},
|
|
165
178
|
{
|
|
166
179
|
name: "delete_messages",
|
|
167
|
-
description: "Send messages to the trash folder, or erase them from this mailbox for good when permanent is true.
|
|
180
|
+
description: "Send messages to the trash folder, or erase them from this mailbox for good when permanent is true. The other party keeps its own copy in its own mailbox.",
|
|
168
181
|
inputSchema: {
|
|
169
182
|
type: "object",
|
|
170
183
|
properties: {
|
|
@@ -276,9 +289,15 @@ const schemas = {
|
|
|
276
289
|
}),
|
|
277
290
|
list_threads: z.object({
|
|
278
291
|
unread_only: z.boolean().optional(),
|
|
292
|
+
with_mailbox: z.string().trim().min(1).optional(),
|
|
293
|
+
// Ancien nom du même filtre, plus publié mais toujours accepté : une session l'a pu
|
|
294
|
+
// apprendre avant la mise à jour. Sans cette entrée, zod l'écarterait en silence et
|
|
295
|
+
// l'agent recevrait tous ses fils en croyant les avoir filtrés.
|
|
296
|
+
with_agent: z.string().trim().min(1).optional(),
|
|
279
297
|
search: z.string().trim().min(1).optional(),
|
|
280
298
|
limit: z.number().int().optional(),
|
|
281
299
|
}),
|
|
300
|
+
list_agents: z.object({ agent_name: z.string().trim().min(1).optional() }),
|
|
282
301
|
create_folder: z.object({ name: z.string().min(1) }),
|
|
283
302
|
delete_folder: z.object({ name: z.string().min(1) }),
|
|
284
303
|
wait_for_message: z.object({ timeout_seconds: z.number().int().optional(), folder: z.string().optional() }),
|
|
@@ -371,39 +390,28 @@ export async function callTool(client, name, rawArgs) {
|
|
|
371
390
|
}
|
|
372
391
|
case "list_threads": {
|
|
373
392
|
const input = schemas.list_threads.parse(args);
|
|
374
|
-
|
|
393
|
+
// Une adresse désigne une boîte : les outils disent `with_mailbox`. Le protocole HTTP
|
|
394
|
+
// et le stockage gardent `with_agent`, du nom de la colonne, comme `thread_title`
|
|
395
|
+
// garde son nom face au `thread_name` des outils.
|
|
396
|
+
const correspondent = input.with_mailbox ?? input.with_agent;
|
|
397
|
+
const threads = await client.listThreads({
|
|
398
|
+
unread_only: input.unread_only,
|
|
399
|
+
search: input.search,
|
|
400
|
+
limit: input.limit,
|
|
401
|
+
with_agent: correspondent,
|
|
402
|
+
});
|
|
403
|
+
const scope = correspondent ? ` with ${correspondent}` : "";
|
|
375
404
|
if (threads.length === 0)
|
|
376
|
-
return
|
|
377
|
-
return [`${threads.length} thread(s):`, "", ...threads.map(formatThread)].join("\n");
|
|
405
|
+
return `No thread in this mailbox${scope} matching these filters.`;
|
|
406
|
+
return [`${threads.length} thread(s)${scope}:`, "", ...threads.map(formatThread)].join("\n");
|
|
378
407
|
}
|
|
379
408
|
case "list_agents": {
|
|
409
|
+
const input = schemas.list_agents.parse(args);
|
|
380
410
|
const agents = await client.listAgents();
|
|
381
411
|
if (agents.length === 0)
|
|
382
|
-
return "No other
|
|
383
|
-
const
|
|
384
|
-
|
|
385
|
-
const flags = agent.registered ? "" : "never connected";
|
|
386
|
-
const names = formatAgentNames(agent);
|
|
387
|
-
const title = names ? `${agent.id} — ${names}` : agent.id;
|
|
388
|
-
// L'annuaire est classé par cette date : l'afficher rend l'ordre lisible.
|
|
389
|
-
const seen = agent.last_activity_at ? `last active ${agent.last_activity_at}` : "never active";
|
|
390
|
-
return [
|
|
391
|
-
`• ${title}${flags ? ` (${flags})` : ""}`,
|
|
392
|
-
` ${agent.agent_description ?? agent.description ?? "no description"}`,
|
|
393
|
-
` ${seen}`,
|
|
394
|
-
].join("\n");
|
|
395
|
-
})
|
|
396
|
-
.join("\n");
|
|
397
|
-
// Un nom d'agent est celui d'un projet, pas d'une session : plusieurs boîtes le
|
|
398
|
-
// portent, y compris celle de l'appelant. Sans ce rappel, un agent croise son propre
|
|
399
|
-
// nom d'agent dans cette liste et prend la boîte d'une autre session pour la sienne.
|
|
400
|
-
return [
|
|
401
|
-
directory,
|
|
402
|
-
"",
|
|
403
|
-
"Addresses identify sessions; agent names do not. Several mailboxes above may carry the same",
|
|
404
|
-
"agent name as yours — they are other sessions of the same project, never you: your own mailbox",
|
|
405
|
-
"is never listed here. Always write to an address taken from this list, never to a name.",
|
|
406
|
-
].join("\n");
|
|
412
|
+
return "No other mailbox on this server yet.";
|
|
413
|
+
const groups = groupByAgentName(agents);
|
|
414
|
+
return input.agent_name ? formatAgentGroup(groups, input.agent_name) : formatDirectory(groups);
|
|
407
415
|
}
|
|
408
416
|
case "send_message": {
|
|
409
417
|
const input = schemas.send_message.parse(args);
|
|
@@ -555,6 +563,111 @@ async function renameMailbox(client, name, description) {
|
|
|
555
563
|
function formatAgentNames(agent) {
|
|
556
564
|
return [agent.agent_name, agent.display_name].filter(Boolean).join(" · ");
|
|
557
565
|
}
|
|
566
|
+
/** Boîtes détaillées sous un nom d'agent avant de renvoyer le reste à `agent_name`. */
|
|
567
|
+
const SESSIONS_SHOWN_PER_AGENT = 4;
|
|
568
|
+
const NO_AGENT_NAME = "unnamed agent";
|
|
569
|
+
/**
|
|
570
|
+
* Deux boîtes sans nom d'agent ne sont pas deux sessions du même projet : rien ne le dit.
|
|
571
|
+
* Chacune reste donc son propre groupe, là où un nom partagé les rassemble.
|
|
572
|
+
*/
|
|
573
|
+
function groupByAgentName(agents) {
|
|
574
|
+
const groups = new Map();
|
|
575
|
+
for (const agent of agents) {
|
|
576
|
+
const name = agent.agent_name?.trim() ?? "";
|
|
577
|
+
const key = name ? name.toLowerCase() : `\u0000${agent.id}`;
|
|
578
|
+
const group = groups.get(key);
|
|
579
|
+
if (group) {
|
|
580
|
+
group.mailboxes.push(agent);
|
|
581
|
+
group.description ??= agent.agent_description;
|
|
582
|
+
}
|
|
583
|
+
else {
|
|
584
|
+
groups.set(key, {
|
|
585
|
+
name: name || NO_AGENT_NAME,
|
|
586
|
+
key,
|
|
587
|
+
description: agent.agent_description,
|
|
588
|
+
mailboxes: [agent],
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
// L'annuaire arrive classé par activité décroissante : l'ordre d'insertion place en tête
|
|
593
|
+
// l'agent dont une session a bougé le plus récemment, et ses boîtes dans le même ordre.
|
|
594
|
+
return [...groups.values()];
|
|
595
|
+
}
|
|
596
|
+
/** `boîte — nom · activité` : une ligne par session, l'adresse en tête car c'est elle qu'on copie. */
|
|
597
|
+
function formatMailboxLine(agent) {
|
|
598
|
+
const seen = agent.last_activity_at ? `last active ${agent.last_activity_at}` : "never active";
|
|
599
|
+
const flags = agent.registered ? "" : " · never connected";
|
|
600
|
+
return ` • ${agent.id} — ${agent.display_name ?? "no mailbox name"} · ${seen}${flags}`;
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* Rappel joint à toute réponse de l'annuaire.
|
|
604
|
+
*
|
|
605
|
+
* Un nom d'agent est celui d'un projet, pas d'une session : plusieurs boîtes le portent, y
|
|
606
|
+
* compris celle de l'appelant. Sans ce rappel, un agent croise son propre nom d'agent dans
|
|
607
|
+
* cette liste et prend la boîte d'une autre session pour la sienne. Le groupement rend le
|
|
608
|
+
* nom encore plus visible que l'adresse : la mise en garde compte d'autant plus.
|
|
609
|
+
*/
|
|
610
|
+
const DIRECTORY_FOOTER = [
|
|
611
|
+
"Addresses identify sessions; agent names do not. Several mailboxes above may carry the same",
|
|
612
|
+
"agent name as yours — they are other sessions of the same project, never you: your own mailbox",
|
|
613
|
+
"is never listed here. Always write to a mailbox address taken from this list, never to a name.",
|
|
614
|
+
"To see what you have already exchanged with one of them, call list_threads with its address in with_mailbox.",
|
|
615
|
+
];
|
|
616
|
+
function formatDirectory(groups) {
|
|
617
|
+
const mailboxes = groups.reduce((total, group) => total + group.mailboxes.length, 0);
|
|
618
|
+
const blocks = groups.map((group) => {
|
|
619
|
+
const sessions = group.mailboxes.length;
|
|
620
|
+
const lines = [`${group.name} — ${sessions} session(s)`];
|
|
621
|
+
if (group.description)
|
|
622
|
+
lines.push(` ${group.description}`);
|
|
623
|
+
// Un agent à sessions nombreuses ne doit pas noyer les autres : les plus récentes
|
|
624
|
+
// suffisent à écrire, `agent_name` donne le reste à qui en a besoin.
|
|
625
|
+
const shown = sessions > SESSIONS_SHOWN_PER_AGENT ? group.mailboxes.slice(0, 3) : group.mailboxes;
|
|
626
|
+
lines.push(...shown.map(formatMailboxLine));
|
|
627
|
+
if (shown.length < sessions) {
|
|
628
|
+
lines.push(` … ${sessions - shown.length} more session(s) — list_agents with agent_name: "${group.name}" lists them all.`);
|
|
629
|
+
}
|
|
630
|
+
return lines.join("\n");
|
|
631
|
+
});
|
|
632
|
+
// Un agent par bloc, séparés par une ligne vide : le groupe est l'unité de lecture.
|
|
633
|
+
return [
|
|
634
|
+
`${groups.length} agent(s), ${mailboxes} mailbox(es):`,
|
|
635
|
+
"",
|
|
636
|
+
blocks.join("\n\n"),
|
|
637
|
+
"",
|
|
638
|
+
...DIRECTORY_FOOTER,
|
|
639
|
+
].join("\n");
|
|
640
|
+
}
|
|
641
|
+
/** Toutes les sessions d'un agent, décrites une par une : le second niveau de l'annuaire. */
|
|
642
|
+
function formatAgentGroup(groups, requested) {
|
|
643
|
+
const wanted = requested.trim().toLowerCase();
|
|
644
|
+
const group = groups.find((g) => g.name.toLowerCase() === wanted);
|
|
645
|
+
if (!group) {
|
|
646
|
+
const known = groups.map((g) => g.name).join(", ");
|
|
647
|
+
throw new Error(`No agent named "${requested}" on this server. Known agent names: ${known}`);
|
|
648
|
+
}
|
|
649
|
+
const blocks = group.mailboxes.map((agent) => {
|
|
650
|
+
const seen = agent.last_activity_at ? `last active ${agent.last_activity_at}` : "never active";
|
|
651
|
+
return [
|
|
652
|
+
`• ${agent.id}`,
|
|
653
|
+
` Mailbox: ${agent.display_name ?? "not named — this session never said what it works on"}`,
|
|
654
|
+
agent.description ? ` ${agent.description}` : null,
|
|
655
|
+
` ${seen}${agent.registered ? "" : " · never connected"}`,
|
|
656
|
+
]
|
|
657
|
+
.filter(Boolean)
|
|
658
|
+
.join("\n");
|
|
659
|
+
});
|
|
660
|
+
return [
|
|
661
|
+
`${group.name} — ${group.mailboxes.length} session(s), most recently active first:`,
|
|
662
|
+
group.description ?? null,
|
|
663
|
+
"",
|
|
664
|
+
...blocks,
|
|
665
|
+
"",
|
|
666
|
+
...DIRECTORY_FOOTER,
|
|
667
|
+
]
|
|
668
|
+
.filter((line) => line !== null)
|
|
669
|
+
.join("\n");
|
|
670
|
+
}
|
|
558
671
|
async function renameThread(client, threadId, name, description) {
|
|
559
672
|
const thread = await client.setThreadLabel(threadId, { title: name, description });
|
|
560
673
|
return [
|
package/dist/src/server/app.js
CHANGED
|
@@ -45,6 +45,7 @@ const threadLabelSchema = z
|
|
|
45
45
|
});
|
|
46
46
|
const listThreadsQuerySchema = z.object({
|
|
47
47
|
unread_only: z.coerce.boolean().optional(),
|
|
48
|
+
with_agent: z.string().trim().min(1).optional(),
|
|
48
49
|
search: z.string().trim().min(1).optional(),
|
|
49
50
|
limit: z.coerce.number().int().min(1).max(100).optional(),
|
|
50
51
|
offset: z.coerce.number().int().min(0).optional(),
|
|
@@ -78,6 +79,14 @@ const waitQuerySchema = z.object({
|
|
|
78
79
|
watcher: z.enum(["true", "1"]).optional(),
|
|
79
80
|
});
|
|
80
81
|
const loginSchema = z.object({ token: z.string().min(1).max(1024) });
|
|
82
|
+
const adminThreadsSchema = z.object({
|
|
83
|
+
search: z.string().trim().min(1).optional(),
|
|
84
|
+
agent: z.string().trim().min(1).optional(),
|
|
85
|
+
unread_only: z.enum(["true", "1"]).optional(),
|
|
86
|
+
priority: z.enum(PRIORITIES).optional(),
|
|
87
|
+
limit: z.coerce.number().int().min(1).max(100).optional(),
|
|
88
|
+
offset: z.coerce.number().int().min(0).optional(),
|
|
89
|
+
});
|
|
81
90
|
const adminListSchema = z.object({
|
|
82
91
|
search: z.string().trim().min(1).optional(),
|
|
83
92
|
agent: z.string().trim().min(1).optional(),
|
|
@@ -161,12 +170,26 @@ export function createApp({ store, config }) {
|
|
|
161
170
|
admin.get("/messages/:id", (req, res) => {
|
|
162
171
|
res.json(store.getMessageAdmin(String(req.params.id)));
|
|
163
172
|
});
|
|
173
|
+
// Déclarée avant `/threads/:id` : la console lit d'abord les fils, le détail ensuite.
|
|
174
|
+
admin.get("/threads", (req, res) => {
|
|
175
|
+
const query = adminThreadsSchema.parse(req.query);
|
|
176
|
+
res.json({
|
|
177
|
+
threads: store.listAllThreadsAdmin({
|
|
178
|
+
search: query.search,
|
|
179
|
+
agent: query.agent,
|
|
180
|
+
unread_only: query.unread_only !== undefined,
|
|
181
|
+
priority: query.priority,
|
|
182
|
+
limit: query.limit,
|
|
183
|
+
offset: query.offset,
|
|
184
|
+
}),
|
|
185
|
+
});
|
|
186
|
+
});
|
|
164
187
|
admin.get("/threads/:id", (req, res) => {
|
|
165
188
|
const threadId = String(req.params.id);
|
|
166
|
-
const messages = store.
|
|
189
|
+
const messages = store.listThreadAdmin(threadId);
|
|
167
190
|
if (messages.length === 0)
|
|
168
191
|
throw new MessengerError(`thread not found: ${threadId}`, 404);
|
|
169
|
-
res.json({ thread_id: threadId, thread: store.getThreadInfo(threadId), messages
|
|
192
|
+
res.json({ thread_id: threadId, thread: store.getThreadInfo(threadId), messages });
|
|
170
193
|
});
|
|
171
194
|
app.use("/admin", admin);
|
|
172
195
|
// Porte d'entrée de tout le reste : aucune requête n'est servie sans jeton valide. Le
|