@imenam/mcp-messenger 1.0.1
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 +59 -0
- package/LICENSE +15 -0
- package/README.md +317 -0
- package/dist/src/cli/setup-mcp.d.ts +6 -0
- package/dist/src/cli/setup-mcp.js +105 -0
- package/dist/src/client/messenger-client.d.ts +78 -0
- package/dist/src/client/messenger-client.js +183 -0
- package/dist/src/config.d.ts +36 -0
- package/dist/src/config.js +110 -0
- package/dist/src/core/ids.d.ts +2 -0
- package/dist/src/core/ids.js +5 -0
- package/dist/src/core/store.d.ts +93 -0
- package/dist/src/core/store.js +527 -0
- package/dist/src/core/types.d.ts +74 -0
- package/dist/src/core/types.js +5 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +53 -0
- package/dist/src/logger.d.ts +6 -0
- package/dist/src/logger.js +22 -0
- package/dist/src/mcp/start.d.ts +16 -0
- package/dist/src/mcp/start.js +76 -0
- package/dist/src/mcp/tools.d.ts +14 -0
- package/dist/src/mcp/tools.js +403 -0
- package/dist/src/server/app.d.ts +15 -0
- package/dist/src/server/app.js +375 -0
- package/dist/src/server/start.d.ts +14 -0
- package/dist/src/server/start.js +64 -0
- package/dist/src/server/ui/app.js +275 -0
- package/dist/src/server/ui/index.html +53 -0
- package/dist/src/server/ui/styles.css +303 -0
- package/dist/src/version.d.ts +1 -0
- package/dist/src/version.js +2 -0
- package/package.json +59 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const LEVELS = { debug: 10, info: 20, warn: 30, error: 40 };
|
|
2
|
+
const threshold = LEVELS[process.env.MESSENGER_LOG_LEVEL in LEVELS ? process.env.MESSENGER_LOG_LEVEL : "info"];
|
|
3
|
+
function write(level, message, extra) {
|
|
4
|
+
if (LEVELS[level] < threshold)
|
|
5
|
+
return;
|
|
6
|
+
const detail = extra instanceof Error ? ` ${extra.stack ?? extra.message}` : extra !== undefined ? ` ${safeStringify(extra)}` : "";
|
|
7
|
+
process.stderr.write(`[messenger] ${new Date().toISOString()} ${level.toUpperCase()} ${message}${detail}\n`);
|
|
8
|
+
}
|
|
9
|
+
function safeStringify(value) {
|
|
10
|
+
try {
|
|
11
|
+
return JSON.stringify(value);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return String(value);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export const logger = {
|
|
18
|
+
debug: (message, extra) => write("debug", message, extra),
|
|
19
|
+
info: (message, extra) => write("info", message, extra),
|
|
20
|
+
warn: (message, extra) => write("warn", message, extra),
|
|
21
|
+
error: (message, extra) => write("error", message, extra),
|
|
22
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
|
+
import { type AgentConfig } from "../config.js";
|
|
3
|
+
import { MessengerClient } from "../client/messenger-client.js";
|
|
4
|
+
/** Serveur MCP prêt à être branché sur n'importe quel transport (stdio en production, mémoire en test). */
|
|
5
|
+
export declare function createMcpServer(client: MessengerClient): Server;
|
|
6
|
+
/**
|
|
7
|
+
* Donne son adresse à la session.
|
|
8
|
+
*
|
|
9
|
+
* Avec `MESSENGER_AGENT_ID`, l'adresse est fixe et partagée par toutes les sessions qui
|
|
10
|
+
* utilisent cette configuration. Sans elle — le cas normal — une boîte neuve est allouée
|
|
11
|
+
* pour cette session : deux agents lancés dans le même projet ne se marchent pas dessus.
|
|
12
|
+
* L'identifiant est opaque : c'est une adresse à transmettre, pas un rôle à deviner.
|
|
13
|
+
*/
|
|
14
|
+
export declare function resolveIdentity(client: MessengerClient, config: AgentConfig): Promise<string>;
|
|
15
|
+
/** Point d'entrée du mode MCP (mode par défaut, sans argument). */
|
|
16
|
+
export declare function runMcpMode(): Promise<void>;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
4
|
+
import { loadAgentConfig } from "../config.js";
|
|
5
|
+
import { MessengerClient, MessengerClientError } from "../client/messenger-client.js";
|
|
6
|
+
import { newId } from "../core/ids.js";
|
|
7
|
+
import { logger } from "../logger.js";
|
|
8
|
+
import { APP_VERSION } from "../version.js";
|
|
9
|
+
import { TOOLS, callTool } from "./tools.js";
|
|
10
|
+
/** Serveur MCP prêt à être branché sur n'importe quel transport (stdio en production, mémoire en test). */
|
|
11
|
+
export function createMcpServer(client) {
|
|
12
|
+
const server = new Server({ name: "mcp-messenger", version: APP_VERSION }, { capabilities: { tools: {} } });
|
|
13
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
|
|
14
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
15
|
+
const { name, arguments: args } = request.params;
|
|
16
|
+
try {
|
|
17
|
+
const text = await callTool(client, name, args);
|
|
18
|
+
return { content: [{ type: "text", text }] };
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
22
|
+
logger.warn(`tool ${name} failed: ${message}`);
|
|
23
|
+
// Une erreur d'outil est une réponse, pas un plantage : l'agent doit pouvoir la lire.
|
|
24
|
+
return { content: [{ type: "text", text: `Error: ${message}` }], isError: true };
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
return server;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Donne son adresse à la session.
|
|
31
|
+
*
|
|
32
|
+
* Avec `MESSENGER_AGENT_ID`, l'adresse est fixe et partagée par toutes les sessions qui
|
|
33
|
+
* utilisent cette configuration. Sans elle — le cas normal — une boîte neuve est allouée
|
|
34
|
+
* pour cette session : deux agents lancés dans le même projet ne se marchent pas dessus.
|
|
35
|
+
* L'identifiant est opaque : c'est une adresse à transmettre, pas un rôle à deviner.
|
|
36
|
+
*/
|
|
37
|
+
export async function resolveIdentity(client, config) {
|
|
38
|
+
if (config.agentId) {
|
|
39
|
+
client.setAgentId(config.agentId, { own: true });
|
|
40
|
+
await client.register();
|
|
41
|
+
return config.agentId;
|
|
42
|
+
}
|
|
43
|
+
// `fresh` fait échouer l'enregistrement si l'adresse existe déjà : la collision, même
|
|
44
|
+
// improbable sur 64 bits d'aléa, ne peut pas faire atterrir deux sessions au même endroit.
|
|
45
|
+
for (let attempt = 0; attempt < 5; attempt += 1) {
|
|
46
|
+
const candidate = newId("box");
|
|
47
|
+
client.setAgentId(candidate, { own: true });
|
|
48
|
+
try {
|
|
49
|
+
await client.register({ fresh: true });
|
|
50
|
+
return candidate;
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
if (error instanceof MessengerClientError && error.status === 409)
|
|
54
|
+
continue;
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
throw new Error("could not allocate a mailbox: too many id collisions");
|
|
59
|
+
}
|
|
60
|
+
/** Point d'entrée du mode MCP (mode par défaut, sans argument). */
|
|
61
|
+
export async function runMcpMode() {
|
|
62
|
+
const config = loadAgentConfig();
|
|
63
|
+
const client = new MessengerClient(config);
|
|
64
|
+
// Un serveur momentanément injoignable ne doit pas empêcher la session MCP de démarrer :
|
|
65
|
+
// l'agent verra l'erreur au premier outil appelé, et pourra réessayer.
|
|
66
|
+
try {
|
|
67
|
+
const agentId = await resolveIdentity(client, config);
|
|
68
|
+
logger.info(`mailbox \`${agentId}\` ready on ${config.serverUrl}`);
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
logger.warn(`could not reach ${config.serverUrl}: ${error instanceof Error ? error.message : String(error)}`);
|
|
72
|
+
}
|
|
73
|
+
const server = createMcpServer(client);
|
|
74
|
+
await server.connect(new StdioServerTransport());
|
|
75
|
+
logger.info(`mcp-messenger v${APP_VERSION} ready (${TOOLS.length} tools)`);
|
|
76
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { MessengerClient } from "../client/messenger-client.js";
|
|
2
|
+
export interface ToolDefinition {
|
|
3
|
+
name: string;
|
|
4
|
+
description: string;
|
|
5
|
+
inputSchema: Record<string, unknown>;
|
|
6
|
+
}
|
|
7
|
+
export declare const TOOLS: ToolDefinition[];
|
|
8
|
+
/**
|
|
9
|
+
* Exécute un outil et renvoie le texte présenté à l'agent.
|
|
10
|
+
*
|
|
11
|
+
* Les erreurs (validation, réseau, refus du serveur) remontent en exception : c'est la
|
|
12
|
+
* couche MCP qui les transforme en réponse `isError`.
|
|
13
|
+
*/
|
|
14
|
+
export declare function callTool(client: MessengerClient, name: string, rawArgs: unknown): Promise<string>;
|
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { PRIORITIES, SYSTEM_FOLDERS } from "../core/types.js";
|
|
3
|
+
const FOLDER_HINT = `Folder name. System folders: ${SYSTEM_FOLDERS.join(", ")}. Custom folders come from list_folders.`;
|
|
4
|
+
export const TOOLS = [
|
|
5
|
+
{
|
|
6
|
+
name: "whoami",
|
|
7
|
+
description: "Return this session's mailbox address, the server it is connected to, and unread counts per folder. The address is what another agent needs in order to write here: give it whenever someone asks how to reach you.",
|
|
8
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
name: "attach_to_mailbox",
|
|
12
|
+
description: "Read and write from another mailbox, given its address — typically to resume the work of an earlier session whose address the user hands over. Call it with no argument to go back to this session's own mailbox.",
|
|
13
|
+
inputSchema: {
|
|
14
|
+
type: "object",
|
|
15
|
+
properties: {
|
|
16
|
+
mailbox_id: {
|
|
17
|
+
type: "string",
|
|
18
|
+
description: "Mailbox address to attach to. Omit to return to this session's own mailbox.",
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
additionalProperties: false,
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
name: "set_mailbox_label",
|
|
26
|
+
description: "Give this mailbox a human-readable label and description, so the user can recognise it in the directory and in the web console. Addresses are opaque: a label is what makes a mailbox findable later.",
|
|
27
|
+
inputSchema: {
|
|
28
|
+
type: "object",
|
|
29
|
+
properties: {
|
|
30
|
+
label: { type: "string", description: "Short name, e.g. 'Refactor du parser'." },
|
|
31
|
+
description: { type: "string", description: "What this session is working on." },
|
|
32
|
+
},
|
|
33
|
+
required: ["label"],
|
|
34
|
+
additionalProperties: false,
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: "list_agents",
|
|
39
|
+
description: "List the agents known to the mailbox server, with their description and last activity. Use it to find the address of the agent you want to write to.",
|
|
40
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: "send_message",
|
|
44
|
+
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.",
|
|
45
|
+
inputSchema: {
|
|
46
|
+
type: "object",
|
|
47
|
+
properties: {
|
|
48
|
+
to: {
|
|
49
|
+
type: "array",
|
|
50
|
+
items: { type: "string" },
|
|
51
|
+
minItems: 1,
|
|
52
|
+
description: "Recipient agent ids (mailbox addresses).",
|
|
53
|
+
},
|
|
54
|
+
cc: { type: "array", items: { type: "string" }, description: "Agents to keep in copy." },
|
|
55
|
+
subject: { type: "string", description: "Short subject line." },
|
|
56
|
+
body: { type: "string", description: "Message body, plain text or markdown." },
|
|
57
|
+
priority: { type: "string", enum: [...PRIORITIES], description: "Defaults to normal." },
|
|
58
|
+
reply_to: { type: "string", description: "Id of the message being answered; the reply joins its thread." },
|
|
59
|
+
},
|
|
60
|
+
required: ["to", "subject", "body"],
|
|
61
|
+
additionalProperties: false,
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
name: "list_messages",
|
|
66
|
+
description: "List messages in this agent's mailbox, newest first, with a short preview. Filter by folder, unread state, sender, thread or free text.",
|
|
67
|
+
inputSchema: {
|
|
68
|
+
type: "object",
|
|
69
|
+
properties: {
|
|
70
|
+
folder: { type: "string", description: FOLDER_HINT + " Defaults to inbox." },
|
|
71
|
+
unread_only: { type: "boolean", description: "Keep only unread messages." },
|
|
72
|
+
from: { type: "string", description: "Only messages sent by this agent id." },
|
|
73
|
+
thread_id: { type: "string", description: "Only messages of this thread." },
|
|
74
|
+
search: { type: "string", description: "Free text matched against subject and body." },
|
|
75
|
+
limit: { type: "number", description: "1-100, defaults to 25." },
|
|
76
|
+
offset: { type: "number", description: "Pagination offset, defaults to 0." },
|
|
77
|
+
},
|
|
78
|
+
additionalProperties: false,
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: "read_message",
|
|
83
|
+
description: "Read one message in full. Marks it as read unless mark_read is false.",
|
|
84
|
+
inputSchema: {
|
|
85
|
+
type: "object",
|
|
86
|
+
properties: {
|
|
87
|
+
message_id: { type: "string", description: "Id returned by list_messages." },
|
|
88
|
+
mark_read: { type: "boolean", description: "Defaults to true." },
|
|
89
|
+
},
|
|
90
|
+
required: ["message_id"],
|
|
91
|
+
additionalProperties: false,
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
name: "get_thread",
|
|
96
|
+
description: "Read every message of a thread visible to this agent, oldest first — the full conversation.",
|
|
97
|
+
inputSchema: {
|
|
98
|
+
type: "object",
|
|
99
|
+
properties: { thread_id: { type: "string", description: "Thread id shown on any message." } },
|
|
100
|
+
required: ["thread_id"],
|
|
101
|
+
additionalProperties: false,
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
name: "mark_messages",
|
|
106
|
+
description: "Mark messages as read or unread, and/or star or unstar them.",
|
|
107
|
+
inputSchema: {
|
|
108
|
+
type: "object",
|
|
109
|
+
properties: {
|
|
110
|
+
message_ids: { type: "array", items: { type: "string" }, minItems: 1 },
|
|
111
|
+
read: { type: "boolean", description: "true = read, false = unread." },
|
|
112
|
+
starred: { type: "boolean", description: "true = starred, false = unstarred." },
|
|
113
|
+
},
|
|
114
|
+
required: ["message_ids"],
|
|
115
|
+
additionalProperties: false,
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
name: "move_messages",
|
|
120
|
+
description: "Move messages to another folder, the way an email client files a conversation.",
|
|
121
|
+
inputSchema: {
|
|
122
|
+
type: "object",
|
|
123
|
+
properties: {
|
|
124
|
+
message_ids: { type: "array", items: { type: "string" }, minItems: 1 },
|
|
125
|
+
folder: { type: "string", description: FOLDER_HINT },
|
|
126
|
+
},
|
|
127
|
+
required: ["message_ids", "folder"],
|
|
128
|
+
additionalProperties: false,
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
name: "delete_messages",
|
|
133
|
+
description: "Send messages to the trash folder, or erase them from this mailbox for good when permanent is true. Other recipients keep their own copy.",
|
|
134
|
+
inputSchema: {
|
|
135
|
+
type: "object",
|
|
136
|
+
properties: {
|
|
137
|
+
message_ids: { type: "array", items: { type: "string" }, minItems: 1 },
|
|
138
|
+
permanent: { type: "boolean", description: "Defaults to false (moves to trash)." },
|
|
139
|
+
},
|
|
140
|
+
required: ["message_ids"],
|
|
141
|
+
additionalProperties: false,
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
name: "list_folders",
|
|
146
|
+
description: "List this mailbox's folders with their total and unread counts.",
|
|
147
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
name: "create_folder",
|
|
151
|
+
description: "Create a custom folder in this mailbox, to sort messages by topic or by correspondent.",
|
|
152
|
+
inputSchema: {
|
|
153
|
+
type: "object",
|
|
154
|
+
properties: { name: { type: "string", description: "1-40 chars: lowercase letters, digits, space, - or _." } },
|
|
155
|
+
required: ["name"],
|
|
156
|
+
additionalProperties: false,
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
name: "delete_folder",
|
|
161
|
+
description: "Delete a custom folder. Its messages go back to inbox (or sent for your own copies).",
|
|
162
|
+
inputSchema: {
|
|
163
|
+
type: "object",
|
|
164
|
+
properties: { name: { type: "string" } },
|
|
165
|
+
required: ["name"],
|
|
166
|
+
additionalProperties: false,
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
name: "wait_for_message",
|
|
171
|
+
description: "Block until a new message lands in the mailbox, or until the timeout expires. Use it to wait for another agent's answer instead of polling list_messages.",
|
|
172
|
+
inputSchema: {
|
|
173
|
+
type: "object",
|
|
174
|
+
properties: {
|
|
175
|
+
timeout_seconds: { type: "number", description: "1-300, defaults to 30." },
|
|
176
|
+
folder: { type: "string", description: "Folder to watch, defaults to inbox." },
|
|
177
|
+
},
|
|
178
|
+
additionalProperties: false,
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
];
|
|
182
|
+
const schemas = {
|
|
183
|
+
send_message: z.object({
|
|
184
|
+
to: z.union([z.string(), z.array(z.string()).min(1)]),
|
|
185
|
+
cc: z.union([z.string(), z.array(z.string())]).optional(),
|
|
186
|
+
subject: z.string().min(1),
|
|
187
|
+
body: z.string().min(1),
|
|
188
|
+
priority: z.enum(PRIORITIES).optional(),
|
|
189
|
+
reply_to: z.string().optional(),
|
|
190
|
+
}),
|
|
191
|
+
list_messages: z.object({
|
|
192
|
+
folder: z.string().optional(),
|
|
193
|
+
unread_only: z.boolean().optional(),
|
|
194
|
+
from: z.string().optional(),
|
|
195
|
+
thread_id: z.string().optional(),
|
|
196
|
+
search: z.string().optional(),
|
|
197
|
+
limit: z.number().int().optional(),
|
|
198
|
+
offset: z.number().int().optional(),
|
|
199
|
+
}),
|
|
200
|
+
read_message: z.object({ message_id: z.string().min(1), mark_read: z.boolean().optional() }),
|
|
201
|
+
get_thread: z.object({ thread_id: z.string().min(1) }),
|
|
202
|
+
mark_messages: z.object({
|
|
203
|
+
message_ids: z.array(z.string().min(1)).min(1),
|
|
204
|
+
read: z.boolean().optional(),
|
|
205
|
+
starred: z.boolean().optional(),
|
|
206
|
+
}),
|
|
207
|
+
move_messages: z.object({ message_ids: z.array(z.string().min(1)).min(1), folder: z.string().min(1) }),
|
|
208
|
+
delete_messages: z.object({ message_ids: z.array(z.string().min(1)).min(1), permanent: z.boolean().optional() }),
|
|
209
|
+
attach_to_mailbox: z.object({ mailbox_id: z.string().trim().min(1).optional() }),
|
|
210
|
+
set_mailbox_label: z.object({ label: z.string().trim().min(1).max(200), description: z.string().max(2000).optional() }),
|
|
211
|
+
create_folder: z.object({ name: z.string().min(1) }),
|
|
212
|
+
delete_folder: z.object({ name: z.string().min(1) }),
|
|
213
|
+
wait_for_message: z.object({ timeout_seconds: z.number().int().optional(), folder: z.string().optional() }),
|
|
214
|
+
};
|
|
215
|
+
/**
|
|
216
|
+
* Exécute un outil et renvoie le texte présenté à l'agent.
|
|
217
|
+
*
|
|
218
|
+
* Les erreurs (validation, réseau, refus du serveur) remontent en exception : c'est la
|
|
219
|
+
* couche MCP qui les transforme en réponse `isError`.
|
|
220
|
+
*/
|
|
221
|
+
export async function callTool(client, name, rawArgs) {
|
|
222
|
+
const args = (rawArgs ?? {});
|
|
223
|
+
switch (name) {
|
|
224
|
+
case "whoami": {
|
|
225
|
+
const stats = await client.stats();
|
|
226
|
+
const health = await client.health().catch(() => null);
|
|
227
|
+
const folders = stats.folders.map((f) => ` ${f.name.padEnd(12)} ${f.total} message(s), ${f.unread} unread`);
|
|
228
|
+
const lines = [
|
|
229
|
+
`Mailbox address: ${client.agentId}`,
|
|
230
|
+
" ↳ give this address to another agent so it can write to you.",
|
|
231
|
+
];
|
|
232
|
+
if (client.isAttachedElsewhere) {
|
|
233
|
+
lines.push(`Attached to another mailbox. This session's own mailbox is ${client.sessionAgentId}.`);
|
|
234
|
+
}
|
|
235
|
+
lines.push(`Mailbox server: ${client.serverUrl}${health ? ` (v${health.version}, ${health.agents} mailbox(es))` : " (unreachable health endpoint)"}`, `Unread (excluding trash): ${stats.unread_total}`, "Folders:", ...folders);
|
|
236
|
+
return lines.join("\n");
|
|
237
|
+
}
|
|
238
|
+
case "attach_to_mailbox": {
|
|
239
|
+
const input = schemas.attach_to_mailbox.parse(args);
|
|
240
|
+
if (!input.mailbox_id) {
|
|
241
|
+
const own = client.sessionAgentId;
|
|
242
|
+
if (!own)
|
|
243
|
+
throw new Error("this session has no mailbox of its own to return to");
|
|
244
|
+
client.setAgentId(own);
|
|
245
|
+
return `Back to this session's own mailbox: ${own}`;
|
|
246
|
+
}
|
|
247
|
+
// Vérifie que la boîte existe avant de basculer : une faute de frappe créerait
|
|
248
|
+
// silencieusement une boîte vide, et l'agent croirait avoir repris le travail.
|
|
249
|
+
const target = await client.getAgent(input.mailbox_id);
|
|
250
|
+
client.setAgentId(target.id);
|
|
251
|
+
const attachedStats = await client.stats();
|
|
252
|
+
return [
|
|
253
|
+
`Attached to mailbox ${target.id}${target.display_name ? ` (${target.display_name})` : ""}.`,
|
|
254
|
+
target.description ? `Description: ${target.description}` : null,
|
|
255
|
+
`Unread (excluding trash): ${attachedStats.unread_total}`,
|
|
256
|
+
client.sessionAgentId && client.sessionAgentId !== target.id
|
|
257
|
+
? `This session's own mailbox remains ${client.sessionAgentId} — attach_to_mailbox with no argument goes back to it.`
|
|
258
|
+
: null,
|
|
259
|
+
]
|
|
260
|
+
.filter(Boolean)
|
|
261
|
+
.join("\n");
|
|
262
|
+
}
|
|
263
|
+
case "set_mailbox_label": {
|
|
264
|
+
const input = schemas.set_mailbox_label.parse(args);
|
|
265
|
+
const agent = await client.updateProfile({ display_name: input.label, description: input.description ?? null });
|
|
266
|
+
return `Mailbox ${agent.id} labelled "${agent.display_name}".`;
|
|
267
|
+
}
|
|
268
|
+
case "list_agents": {
|
|
269
|
+
const agents = await client.listAgents();
|
|
270
|
+
if (agents.length === 0)
|
|
271
|
+
return "No agent registered yet on this server.";
|
|
272
|
+
return agents
|
|
273
|
+
.map((agent) => {
|
|
274
|
+
const flags = [agent.id === client.agentId ? "you" : null, agent.registered ? null : "never connected"]
|
|
275
|
+
.filter(Boolean)
|
|
276
|
+
.join(", ");
|
|
277
|
+
const title = agent.display_name ? `${agent.id} — ${agent.display_name}` : agent.id;
|
|
278
|
+
const seen = agent.last_seen_at ? `last seen ${agent.last_seen_at}` : "never seen";
|
|
279
|
+
return [`• ${title}${flags ? ` (${flags})` : ""}`, ` ${agent.description ?? "no description"}`, ` ${seen}`].join("\n");
|
|
280
|
+
})
|
|
281
|
+
.join("\n");
|
|
282
|
+
}
|
|
283
|
+
case "send_message": {
|
|
284
|
+
const input = schemas.send_message.parse(args);
|
|
285
|
+
const result = await client.sendMessage({
|
|
286
|
+
to: asArray(input.to),
|
|
287
|
+
cc: asArray(input.cc),
|
|
288
|
+
subject: input.subject,
|
|
289
|
+
body: input.body,
|
|
290
|
+
priority: input.priority,
|
|
291
|
+
reply_to: input.reply_to ?? null,
|
|
292
|
+
});
|
|
293
|
+
const lines = [
|
|
294
|
+
`Message sent: ${result.message_id}`,
|
|
295
|
+
`Thread: ${result.thread_id}`,
|
|
296
|
+
`Delivered to: ${result.delivered_to.join(", ")}`,
|
|
297
|
+
];
|
|
298
|
+
if (result.unknown_recipients.length > 0) {
|
|
299
|
+
lines.push(`Warning — never connected to this server, the message will wait in their mailbox: ${result.unknown_recipients.join(", ")}`);
|
|
300
|
+
}
|
|
301
|
+
return lines.join("\n");
|
|
302
|
+
}
|
|
303
|
+
case "list_messages": {
|
|
304
|
+
const input = schemas.list_messages.parse(args);
|
|
305
|
+
const folder = input.folder ?? "inbox";
|
|
306
|
+
const messages = await client.listMessages({ ...input, folder });
|
|
307
|
+
if (messages.length === 0)
|
|
308
|
+
return `No message in \`${folder}\` matching these filters.`;
|
|
309
|
+
return [`${messages.length} message(s) in \`${folder}\`:`, "", ...messages.map(formatSummary)].join("\n");
|
|
310
|
+
}
|
|
311
|
+
case "read_message": {
|
|
312
|
+
const input = schemas.read_message.parse(args);
|
|
313
|
+
const message = await client.getMessage(input.message_id, input.mark_read ?? true);
|
|
314
|
+
return formatMessage(message);
|
|
315
|
+
}
|
|
316
|
+
case "get_thread": {
|
|
317
|
+
const input = schemas.get_thread.parse(args);
|
|
318
|
+
const messages = await client.getThread(input.thread_id);
|
|
319
|
+
return [`Thread ${input.thread_id} — ${messages.length} message(s):`, "", ...messages.map(formatMessage)].join("\n\n---\n\n");
|
|
320
|
+
}
|
|
321
|
+
case "mark_messages": {
|
|
322
|
+
const input = schemas.mark_messages.parse(args);
|
|
323
|
+
if (input.read === undefined && input.starred === undefined) {
|
|
324
|
+
throw new Error("provide `read` and/or `starred`");
|
|
325
|
+
}
|
|
326
|
+
const updated = await client.markMessages(input.message_ids, { read: input.read, starred: input.starred });
|
|
327
|
+
const changes = [
|
|
328
|
+
input.read === undefined ? null : input.read ? "read" : "unread",
|
|
329
|
+
input.starred === undefined ? null : input.starred ? "starred" : "unstarred",
|
|
330
|
+
].filter(Boolean);
|
|
331
|
+
return `${updated} message(s) marked ${changes.join(" + ")}.`;
|
|
332
|
+
}
|
|
333
|
+
case "move_messages": {
|
|
334
|
+
const input = schemas.move_messages.parse(args);
|
|
335
|
+
const updated = await client.moveMessages(input.message_ids, input.folder);
|
|
336
|
+
return `${updated} message(s) moved to \`${input.folder}\`.`;
|
|
337
|
+
}
|
|
338
|
+
case "delete_messages": {
|
|
339
|
+
const input = schemas.delete_messages.parse(args);
|
|
340
|
+
const deleted = await client.deleteMessages(input.message_ids, input.permanent ?? false);
|
|
341
|
+
return input.permanent
|
|
342
|
+
? `${deleted} message(s) permanently removed from this mailbox.`
|
|
343
|
+
: `${deleted} message(s) moved to trash.`;
|
|
344
|
+
}
|
|
345
|
+
case "list_folders": {
|
|
346
|
+
const folders = await client.listFolders();
|
|
347
|
+
return folders
|
|
348
|
+
.map((f) => `${f.name.padEnd(14)} ${String(f.total).padStart(4)} message(s), ${f.unread} unread${f.system ? " (system)" : ""}`)
|
|
349
|
+
.join("\n");
|
|
350
|
+
}
|
|
351
|
+
case "create_folder": {
|
|
352
|
+
const input = schemas.create_folder.parse(args);
|
|
353
|
+
const folder = await client.createFolder(input.name);
|
|
354
|
+
return `Folder \`${folder.name}\` created.`;
|
|
355
|
+
}
|
|
356
|
+
case "delete_folder": {
|
|
357
|
+
const input = schemas.delete_folder.parse(args);
|
|
358
|
+
const moved = await client.deleteFolder(input.name);
|
|
359
|
+
return `Folder \`${input.name}\` deleted; ${moved} message(s) moved back to inbox.`;
|
|
360
|
+
}
|
|
361
|
+
case "wait_for_message": {
|
|
362
|
+
const input = schemas.wait_for_message.parse(args);
|
|
363
|
+
const timeout = Math.min(Math.max(input.timeout_seconds ?? 30, 1), 300);
|
|
364
|
+
const result = await client.waitForMessage(timeout, input.folder);
|
|
365
|
+
if (result.timed_out || !result.message) {
|
|
366
|
+
return `No new message after ${timeout}s.`;
|
|
367
|
+
}
|
|
368
|
+
return ["New message:", "", formatMessage(result.message)].join("\n");
|
|
369
|
+
}
|
|
370
|
+
default:
|
|
371
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
function asArray(value) {
|
|
375
|
+
if (value === undefined)
|
|
376
|
+
return [];
|
|
377
|
+
return Array.isArray(value) ? value : [value];
|
|
378
|
+
}
|
|
379
|
+
function formatSummary(message) {
|
|
380
|
+
const flags = [message.read ? " " : "•", message.starred ? "★" : " "].join("");
|
|
381
|
+
const priority = message.priority === "normal" ? "" : ` [${message.priority}]`;
|
|
382
|
+
return [
|
|
383
|
+
`${flags} ${message.subject}${priority}`,
|
|
384
|
+
` from ${message.from} to ${message.to.join(", ") || "—"}${message.cc.length ? ` (cc ${message.cc.join(", ")})` : ""} · ${message.created_at}`,
|
|
385
|
+
` ${message.preview}`,
|
|
386
|
+
` id ${message.message_id} · thread ${message.thread_id}`,
|
|
387
|
+
].join("\n");
|
|
388
|
+
}
|
|
389
|
+
function formatMessage(message) {
|
|
390
|
+
const header = [
|
|
391
|
+
`Subject: ${message.subject}`,
|
|
392
|
+
`From: ${message.from}`,
|
|
393
|
+
`To: ${message.to.join(", ") || "—"}`,
|
|
394
|
+
message.cc.length ? `Cc: ${message.cc.join(", ")}` : null,
|
|
395
|
+
`Date: ${message.created_at}`,
|
|
396
|
+
`Priority: ${message.priority}`,
|
|
397
|
+
`Folder: ${message.folder}${message.read ? "" : " (unread)"}${message.starred ? " ★" : ""}`,
|
|
398
|
+
`Id: ${message.message_id} · Thread: ${message.thread_id}${message.in_reply_to ? ` · reply to ${message.in_reply_to}` : ""}`,
|
|
399
|
+
]
|
|
400
|
+
.filter(Boolean)
|
|
401
|
+
.join("\n");
|
|
402
|
+
return `${header}\n\n${message.body}`;
|
|
403
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import express from "express";
|
|
2
|
+
import type { ServerConfig } from "../config.js";
|
|
3
|
+
import { MessageStore } from "../core/store.js";
|
|
4
|
+
declare global {
|
|
5
|
+
namespace Express {
|
|
6
|
+
interface Request {
|
|
7
|
+
agentId?: string;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export interface AppOptions {
|
|
12
|
+
store: MessageStore;
|
|
13
|
+
config: ServerConfig;
|
|
14
|
+
}
|
|
15
|
+
export declare function createApp({ store, config }: AppOptions): express.Express;
|