@imenam/mcp-messenger 1.0.29 → 1.0.31
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 +764 -707
- package/dist/src/cli/watch.d.ts +3 -1
- package/dist/src/cli/watch.js +12 -2
- package/dist/src/client/messenger-client.d.ts +21 -1
- package/dist/src/client/messenger-client.js +31 -9
- package/dist/src/core/attachments.d.ts +16 -0
- package/dist/src/core/attachments.js +28 -0
- package/dist/src/core/store.d.ts +34 -3
- package/dist/src/core/store.js +404 -216
- package/dist/src/core/types.d.ts +41 -0
- package/dist/src/mcp/start.js +25 -22
- package/dist/src/mcp/tools.js +186 -9
- package/dist/src/server/app.js +89 -12
- package/dist/src/server/ui/app.js +867 -7
- package/dist/src/server/ui/index.html +59 -0
- package/dist/src/server/ui/styles.css +751 -20
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +16 -2
- package/package.json +2 -2
package/dist/src/server/app.js
CHANGED
|
@@ -5,6 +5,7 @@ import { fileURLToPath } from "node:url";
|
|
|
5
5
|
import { timingSafeEqual } from "node:crypto";
|
|
6
6
|
import { rateLimit } from "express-rate-limit";
|
|
7
7
|
import { z } from "zod";
|
|
8
|
+
import { MAX_ATTACHMENTS, MAX_ATTACHMENTS_TOTAL_BYTES, contentDisposition } from "../core/attachments.js";
|
|
8
9
|
import { MessengerError, normalizeAgentId } from "../core/store.js";
|
|
9
10
|
import { PRIORITIES } from "../core/types.js";
|
|
10
11
|
import { WatcherRegistry } from "./watchers.js";
|
|
@@ -12,6 +13,9 @@ import { CONSOLE_COOKIE, ConsoleSessions, consoleSessionId } from "./console-ses
|
|
|
12
13
|
import { APP_VERSION } from "../version.js";
|
|
13
14
|
import { logger } from "../logger.js";
|
|
14
15
|
const UI_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), "ui");
|
|
16
|
+
const JSON_TYPES = ["application/json", "application/*+json"];
|
|
17
|
+
/** Corps d'un envoi : le texte, plus les pièces jointes encodées en base64 (4 octets pour 3). */
|
|
18
|
+
const SEND_BODY_LIMIT = Math.ceil((MAX_ATTACHMENTS_TOTAL_BYTES * 4) / 3) + 1024 * 1024;
|
|
15
19
|
const agentIdSchema = z.string().trim().min(1).max(128);
|
|
16
20
|
const idListSchema = z.array(z.string().trim().min(1)).min(1).max(200);
|
|
17
21
|
/** Nom et description de la boîte : ce que l'agent pose lui-même sur sa session. */
|
|
@@ -34,6 +38,14 @@ const sendSchema = z.object({
|
|
|
34
38
|
reply_to: z.string().trim().min(1).nullish(),
|
|
35
39
|
thread_title: z.string().trim().min(1).max(200).nullish(),
|
|
36
40
|
thread_description: z.string().trim().min(1).max(2000).nullish(),
|
|
41
|
+
attachments: z
|
|
42
|
+
.array(z.object({
|
|
43
|
+
filename: z.string().trim().min(1).max(255),
|
|
44
|
+
mime_type: z.string().trim().max(255).nullish(),
|
|
45
|
+
content_base64: z.string().regex(/^[A-Za-z0-9+/]*={0,2}$/, "expected base64 content"),
|
|
46
|
+
}))
|
|
47
|
+
.max(MAX_ATTACHMENTS)
|
|
48
|
+
.optional(),
|
|
37
49
|
});
|
|
38
50
|
const threadLabelSchema = z
|
|
39
51
|
.object({
|
|
@@ -121,6 +133,10 @@ const purgeSchema = z
|
|
|
121
133
|
.refine((input) => [input.before, input.created_before, input.empty].filter((value) => value !== undefined).length === 1, {
|
|
122
134
|
message: "provide exactly one of `before` (inactive since that date), `created_before` (created before that date) or `empty: true`",
|
|
123
135
|
});
|
|
136
|
+
const linkSchema = z.object({
|
|
137
|
+
mailboxes: z.tuple([agentIdSchema, agentIdSchema]),
|
|
138
|
+
note: z.string().trim().max(500).nullish(),
|
|
139
|
+
});
|
|
124
140
|
const adminListSchema = z.object({
|
|
125
141
|
search: z.string().trim().min(1).optional(),
|
|
126
142
|
agent: z.string().trim().min(1).optional(),
|
|
@@ -151,7 +167,10 @@ export function createApp({ store, config }) {
|
|
|
151
167
|
},
|
|
152
168
|
crossOriginEmbedderPolicy: false,
|
|
153
169
|
}));
|
|
154
|
-
|
|
170
|
+
// L'envoi d'un message a sa propre limite, plus large pour ses pièces jointes, et ne
|
|
171
|
+
// s'applique qu'une fois l'agent authentifié.
|
|
172
|
+
const jsonBody = express.json({ type: JSON_TYPES, limit: "2mb" });
|
|
173
|
+
app.use((req, res, next) => (isSendRequest(req) ? next() : jsonBody(req, res, next)));
|
|
155
174
|
app.use(rateLimit({
|
|
156
175
|
windowMs: config.rateLimit.windowMs,
|
|
157
176
|
limit: config.rateLimit.max,
|
|
@@ -194,7 +213,7 @@ export function createApp({ store, config }) {
|
|
|
194
213
|
app.get("/", (_req, res) => {
|
|
195
214
|
res.redirect("/ui/");
|
|
196
215
|
});
|
|
197
|
-
// --- Supervision (lecture,
|
|
216
|
+
// --- Supervision (lecture, liaisons et nettoyages de boîtes) -------------
|
|
198
217
|
// Montée avant la porte d'entrée : son contrôle est au moins aussi strict (session de
|
|
199
218
|
// console ou jeton admin), et la session par cookie n'est reconnue nulle part ailleurs.
|
|
200
219
|
const admin = express.Router();
|
|
@@ -209,6 +228,9 @@ export function createApp({ store, config }) {
|
|
|
209
228
|
admin.get("/messages/:id", (req, res) => {
|
|
210
229
|
res.json(store.getMessageAdmin(String(req.params.id)));
|
|
211
230
|
});
|
|
231
|
+
admin.get("/attachments/:id", (req, res) => {
|
|
232
|
+
sendAttachment(res, store.getAttachmentAdmin(String(req.params.id)));
|
|
233
|
+
});
|
|
212
234
|
// Déclarée avant `/threads/:id` : la console lit d'abord les fils, le détail ensuite.
|
|
213
235
|
admin.get("/threads", (req, res) => {
|
|
214
236
|
const query = adminThreadsSchema.parse(req.query);
|
|
@@ -263,7 +285,7 @@ export function createApp({ store, config }) {
|
|
|
263
285
|
res.json({ mailboxes: store.emptyMailboxes() });
|
|
264
286
|
});
|
|
265
287
|
/**
|
|
266
|
-
*
|
|
288
|
+
* Suppression en lot : elle détruit.
|
|
267
289
|
*
|
|
268
290
|
* Deux garde-fous, parce qu'un nettoyage ne se rattrape pas : le critère est redemandé
|
|
269
291
|
* avec les adresses, et le serveur refuse toute adresse qui n'y répond plus au moment
|
|
@@ -313,6 +335,21 @@ export function createApp({ store, config }) {
|
|
|
313
335
|
logger.warn(`admin delete: mailbox ${id} deleted (${removed.messages} message(s) erased)`);
|
|
314
336
|
res.json({ mailbox: id, ...removed });
|
|
315
337
|
});
|
|
338
|
+
admin.get("/links", (_req, res) => {
|
|
339
|
+
res.json({ links: store.listLinks() });
|
|
340
|
+
});
|
|
341
|
+
admin.post("/links", (req, res) => {
|
|
342
|
+
const input = linkSchema.parse(req.body ?? {});
|
|
343
|
+
const link = store.createLink(input.mailboxes[0], input.mailboxes[1], input.note ?? null);
|
|
344
|
+
logger.info(`admin link ${link.link_id}: ${link.mailboxes[0].mailbox} <-> ${link.mailboxes[1].mailbox}`);
|
|
345
|
+
res.status(201).json({ link });
|
|
346
|
+
});
|
|
347
|
+
admin.delete("/links/:id", (req, res) => {
|
|
348
|
+
const linkId = String(req.params.id);
|
|
349
|
+
store.deleteLink(linkId);
|
|
350
|
+
logger.info(`admin unlink ${linkId}`);
|
|
351
|
+
res.json({ link_id: linkId });
|
|
352
|
+
});
|
|
316
353
|
app.use("/admin", admin);
|
|
317
354
|
// Porte d'entrée de tout le reste : aucune requête n'est servie sans jeton valide. Le
|
|
318
355
|
// contrôle par route (identité de l'agent) vient ensuite.
|
|
@@ -370,10 +407,14 @@ export function createApp({ store, config }) {
|
|
|
370
407
|
throw new MessengerError(`mailbox not found: ${req.params.id}`, 404);
|
|
371
408
|
res.json({ agent });
|
|
372
409
|
});
|
|
410
|
+
// Lire ses liaisons, c'est en prendre connaissance : la console l'affiche comme reçue.
|
|
411
|
+
api.get("/links", (req, res) => {
|
|
412
|
+
res.json({ links: store.linksFor(req.agentId, { markSeen: true }) });
|
|
413
|
+
});
|
|
373
414
|
api.get("/mailbox", (req, res) => {
|
|
374
415
|
res.json({ ...store.stats(req.agentId), watcher: watchers.status(req.agentId) });
|
|
375
416
|
});
|
|
376
|
-
api.post("/messages", (req, res) => {
|
|
417
|
+
api.post("/messages", express.json({ type: JSON_TYPES, limit: SEND_BODY_LIMIT }), (req, res) => {
|
|
377
418
|
const input = sendSchema.parse(req.body ?? {});
|
|
378
419
|
const result = store.sendMessage({
|
|
379
420
|
from: req.agentId,
|
|
@@ -384,6 +425,11 @@ export function createApp({ store, config }) {
|
|
|
384
425
|
reply_to: input.reply_to ?? null,
|
|
385
426
|
thread_title: input.thread_title ?? null,
|
|
386
427
|
thread_description: input.thread_description ?? null,
|
|
428
|
+
attachments: (input.attachments ?? []).map((a) => ({
|
|
429
|
+
filename: a.filename,
|
|
430
|
+
mime_type: a.mime_type ?? null,
|
|
431
|
+
data: Buffer.from(a.content_base64, "base64"),
|
|
432
|
+
})),
|
|
387
433
|
});
|
|
388
434
|
logger.info(`message ${result.message_id}: ${req.agentId} -> ${result.delivered_to}`);
|
|
389
435
|
res.status(201).json(result);
|
|
@@ -409,6 +455,9 @@ export function createApp({ store, config }) {
|
|
|
409
455
|
const markRead = req.query.mark_read !== "false";
|
|
410
456
|
res.json(store.getMessage(req.agentId, String(req.params.id), markRead));
|
|
411
457
|
});
|
|
458
|
+
api.get("/attachments/:id", (req, res) => {
|
|
459
|
+
sendAttachment(res, store.getAttachment(req.agentId, String(req.params.id)));
|
|
460
|
+
});
|
|
412
461
|
api.get("/threads", (req, res) => {
|
|
413
462
|
const query = listThreadsQuerySchema.parse(req.query);
|
|
414
463
|
res.json({ threads: store.listThreads(req.agentId, query) });
|
|
@@ -434,7 +483,8 @@ export function createApp({ store, config }) {
|
|
|
434
483
|
});
|
|
435
484
|
// Attente longue : seuls les messages arrivés APRÈS le début de l'attente réveillent
|
|
436
485
|
// l'appelant ; les non-lus déjà présents sont ignorés (sinon l'outil rendrait la main
|
|
437
|
-
// immédiatement tant que la boîte n'est pas vidée).
|
|
486
|
+
// immédiatement tant que la boîte n'est pas vidée). Un veilleur est aussi réveillé par
|
|
487
|
+
// une liaison posée sur sa boîte.
|
|
438
488
|
api.get("/wait", (req, res) => {
|
|
439
489
|
const { timeout_seconds, folder, watcher } = waitQuerySchema.parse(req.query);
|
|
440
490
|
const agentId = req.agentId;
|
|
@@ -445,11 +495,15 @@ export function createApp({ store, config }) {
|
|
|
445
495
|
// Le relâchement doit couvrir les trois sorties possibles : message, expiration,
|
|
446
496
|
// client parti. Un compteur qui ne redescend pas ferait croire à un veilleur éternel.
|
|
447
497
|
const releaseWatcher = watcher ? watchers.begin(agentId) : () => { };
|
|
448
|
-
const
|
|
498
|
+
const stop = () => {
|
|
449
499
|
clearTimeout(timer);
|
|
450
500
|
store.off("message", onMessage);
|
|
451
|
-
|
|
501
|
+
store.off("link", onLink);
|
|
502
|
+
req.off("close", stop);
|
|
452
503
|
releaseWatcher();
|
|
504
|
+
};
|
|
505
|
+
const done = (payload) => {
|
|
506
|
+
stop();
|
|
453
507
|
if (!res.headersSent)
|
|
454
508
|
res.json(payload);
|
|
455
509
|
};
|
|
@@ -462,14 +516,17 @@ export function createApp({ store, config }) {
|
|
|
462
516
|
return;
|
|
463
517
|
done({ timed_out: false, message: store.getMessage(agentId, event.message_id, false) });
|
|
464
518
|
};
|
|
465
|
-
const
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
519
|
+
const onLink = (event) => {
|
|
520
|
+
if (event.agent_id !== agentId)
|
|
521
|
+
return;
|
|
522
|
+
const [link] = store.linksFor(agentId, { linkId: event.link_id });
|
|
523
|
+
done({ timed_out: false, message: null, link });
|
|
469
524
|
};
|
|
470
525
|
const timer = setTimeout(() => done({ timed_out: true, message: null }), timeoutSeconds * 1000);
|
|
471
526
|
store.on("message", onMessage);
|
|
472
|
-
|
|
527
|
+
if (watcher)
|
|
528
|
+
store.on("link", onLink);
|
|
529
|
+
req.once("close", stop);
|
|
473
530
|
});
|
|
474
531
|
app.use("/api", api);
|
|
475
532
|
// --- Erreurs --------------------------------------------------------------
|
|
@@ -485,12 +542,32 @@ export function createApp({ store, config }) {
|
|
|
485
542
|
res.status(400).json({ error: "invalid request", details: z.treeifyError(err) });
|
|
486
543
|
return;
|
|
487
544
|
}
|
|
545
|
+
// Erreurs du lecteur de corps (trop gros, JSON illisible) : leur message est fait pour être lu.
|
|
546
|
+
if (isExposedHttpError(err)) {
|
|
547
|
+
res.status(err.status).json({ error: err.message });
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
488
550
|
logger.error("unhandled error", err);
|
|
489
551
|
// Le détail reste dans les logs : la réponse ne doit rien révéler de l'hôte.
|
|
490
552
|
res.status(500).json({ error: "internal server error" });
|
|
491
553
|
});
|
|
492
554
|
return app;
|
|
493
555
|
}
|
|
556
|
+
function isExposedHttpError(err) {
|
|
557
|
+
if (typeof err !== "object" || err === null)
|
|
558
|
+
return false;
|
|
559
|
+
const { expose, status } = err;
|
|
560
|
+
return expose === true && typeof status === "number" && status >= 400 && status < 500;
|
|
561
|
+
}
|
|
562
|
+
function isSendRequest(req) {
|
|
563
|
+
return req.method === "POST" && req.path === "/api/messages";
|
|
564
|
+
}
|
|
565
|
+
function sendAttachment(res, attachment) {
|
|
566
|
+
res.setHeader("content-type", attachment.mime_type);
|
|
567
|
+
res.setHeader("content-length", String(attachment.data.length));
|
|
568
|
+
res.setHeader("content-disposition", contentDisposition(attachment.filename));
|
|
569
|
+
res.end(attachment.data);
|
|
570
|
+
}
|
|
494
571
|
/**
|
|
495
572
|
* Exige un jeton connu du serveur sur toute requête entrante.
|
|
496
573
|
*
|