@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,275 @@
|
|
|
1
|
+
// Console de supervision : lecture seule sur les routes /admin du serveur.
|
|
2
|
+
// Tout le contenu affiché provient d'agents : il est inséré via textContent uniquement.
|
|
3
|
+
|
|
4
|
+
const TOKEN_KEY = "messenger.admin.token";
|
|
5
|
+
const REFRESH_MS = 5000;
|
|
6
|
+
|
|
7
|
+
// Le serveur exige un jeton sur toute requête, y compris celle qui a servi cette page :
|
|
8
|
+
// il est propagé sur l'URL de ce script. On le reprend tel quel comme jeton de session.
|
|
9
|
+
const TOKEN_FROM_URL = new URL(import.meta.url).searchParams.get("token");
|
|
10
|
+
|
|
11
|
+
const state = {
|
|
12
|
+
token: TOKEN_FROM_URL || localStorage.getItem(TOKEN_KEY) || "",
|
|
13
|
+
agent: null,
|
|
14
|
+
search: "",
|
|
15
|
+
selectedMessageId: null,
|
|
16
|
+
threadId: null,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const el = {
|
|
20
|
+
statusDot: document.getElementById("status-dot"),
|
|
21
|
+
serverStats: document.getElementById("server-stats"),
|
|
22
|
+
agents: document.getElementById("agents"),
|
|
23
|
+
messages: document.getElementById("messages"),
|
|
24
|
+
detail: document.getElementById("detail"),
|
|
25
|
+
search: document.getElementById("search"),
|
|
26
|
+
filterLabel: document.getElementById("filter-label"),
|
|
27
|
+
clearFilters: document.getElementById("clear-filters"),
|
|
28
|
+
refresh: document.getElementById("refresh"),
|
|
29
|
+
autoRefresh: document.getElementById("auto-refresh"),
|
|
30
|
+
tokenBar: document.getElementById("token-bar"),
|
|
31
|
+
tokenInput: document.getElementById("token"),
|
|
32
|
+
tokenError: document.getElementById("token-error"),
|
|
33
|
+
saveToken: document.getElementById("save-token"),
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
async function api(path, params) {
|
|
37
|
+
const url = new URL(path, window.location.origin);
|
|
38
|
+
for (const [key, value] of Object.entries(params || {})) {
|
|
39
|
+
if (value !== undefined && value !== null && value !== "") url.searchParams.set(key, value);
|
|
40
|
+
}
|
|
41
|
+
const headers = { accept: "application/json" };
|
|
42
|
+
if (state.token) headers.authorization = "Bearer " + state.token;
|
|
43
|
+
|
|
44
|
+
const response = await fetch(url, { headers });
|
|
45
|
+
if (response.status === 401) {
|
|
46
|
+
el.tokenBar.hidden = false;
|
|
47
|
+
throw new Error("unauthorized");
|
|
48
|
+
}
|
|
49
|
+
if (!response.ok) {
|
|
50
|
+
const payload = await response.json().catch(() => ({}));
|
|
51
|
+
throw new Error(payload.error || response.statusText);
|
|
52
|
+
}
|
|
53
|
+
return response.json();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Remplace le contenu du panneau de détail (et retire l'état « vide » hérité du HTML). */
|
|
57
|
+
function setDetail(child, empty) {
|
|
58
|
+
el.detail.className = empty ? "empty" : "detail-content";
|
|
59
|
+
el.detail.replaceChildren(child);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function node(tag, className, text) {
|
|
63
|
+
const element = document.createElement(tag);
|
|
64
|
+
if (className) element.className = className;
|
|
65
|
+
if (text !== undefined) element.textContent = text;
|
|
66
|
+
return element;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function formatDate(iso) {
|
|
70
|
+
const date = new Date(iso);
|
|
71
|
+
return Number.isNaN(date.getTime()) ? iso : date.toLocaleString();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function renderAgents(overview) {
|
|
75
|
+
el.serverStats.textContent =
|
|
76
|
+
overview.messages + " message(s) · " + overview.threads + " fil(s) · " + overview.agents.length + " agent(s)";
|
|
77
|
+
el.statusDot.className = "dot ok";
|
|
78
|
+
el.agents.replaceChildren();
|
|
79
|
+
|
|
80
|
+
for (const agent of overview.agents) {
|
|
81
|
+
const item = node("li", state.agent === agent.id ? "selected" : "");
|
|
82
|
+
item.appendChild(node("div", "id", agent.display_name ? agent.id + " — " + agent.display_name : agent.id));
|
|
83
|
+
item.appendChild(
|
|
84
|
+
node("div", "muted", "envoyés " + agent.sent + " · reçus " + agent.received + " · non lus " + agent.unread)
|
|
85
|
+
);
|
|
86
|
+
if (!agent.registered) item.appendChild(node("div", "muted", "jamais connecté"));
|
|
87
|
+
item.addEventListener("click", () => {
|
|
88
|
+
state.agent = state.agent === agent.id ? null : agent.id;
|
|
89
|
+
state.threadId = null;
|
|
90
|
+
refresh();
|
|
91
|
+
});
|
|
92
|
+
el.agents.appendChild(item);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function renderMessages(messages) {
|
|
97
|
+
el.filterLabel.textContent = [
|
|
98
|
+
state.agent ? "agent : " + state.agent : null,
|
|
99
|
+
state.threadId ? "fil : " + state.threadId : null,
|
|
100
|
+
]
|
|
101
|
+
.filter(Boolean)
|
|
102
|
+
.join(" · ");
|
|
103
|
+
|
|
104
|
+
el.messages.replaceChildren();
|
|
105
|
+
if (messages.length === 0) {
|
|
106
|
+
el.messages.appendChild(node("li", "empty", "Aucun message."));
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
for (const message of messages) {
|
|
111
|
+
const item = node("li", state.selectedMessageId === message.message_id ? "selected" : "");
|
|
112
|
+
const subject = node("div", "subject", message.subject);
|
|
113
|
+
if (message.priority !== "normal") {
|
|
114
|
+
const badge = node("span", "badge" + (message.priority === "high" ? " high" : ""), message.priority);
|
|
115
|
+
subject.appendChild(badge);
|
|
116
|
+
}
|
|
117
|
+
item.appendChild(subject);
|
|
118
|
+
item.appendChild(
|
|
119
|
+
node("div", "route", message.from + " → " + (message.to.join(", ") || "—") + " · " + formatDate(message.created_at))
|
|
120
|
+
);
|
|
121
|
+
item.appendChild(node("div", "preview", message.preview));
|
|
122
|
+
item.addEventListener("click", () => openMessage(message.message_id));
|
|
123
|
+
el.messages.appendChild(item);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function openMessage(messageId) {
|
|
128
|
+
state.selectedMessageId = messageId;
|
|
129
|
+
try {
|
|
130
|
+
const message = await api("/admin/messages/" + encodeURIComponent(messageId));
|
|
131
|
+
renderDetail(message);
|
|
132
|
+
await refresh({ keepDetail: true });
|
|
133
|
+
} catch (error) {
|
|
134
|
+
setDetail(node("div", "error", error.message), false);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function renderDetail(message) {
|
|
139
|
+
const container = node("div", "detail");
|
|
140
|
+
container.appendChild(node("h3", null, message.subject));
|
|
141
|
+
container.appendChild(
|
|
142
|
+
node(
|
|
143
|
+
"div",
|
|
144
|
+
"meta",
|
|
145
|
+
"De " +
|
|
146
|
+
message.from +
|
|
147
|
+
" · À " +
|
|
148
|
+
(message.to.join(", ") || "—") +
|
|
149
|
+
(message.cc.length ? " · Cc " + message.cc.join(", ") : "") +
|
|
150
|
+
"\n" +
|
|
151
|
+
formatDate(message.created_at) +
|
|
152
|
+
" · priorité " +
|
|
153
|
+
message.priority +
|
|
154
|
+
"\nid " +
|
|
155
|
+
message.message_id +
|
|
156
|
+
" · fil " +
|
|
157
|
+
message.thread_id
|
|
158
|
+
)
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
const threadButton = node("button", null, "Voir le fil complet");
|
|
162
|
+
threadButton.addEventListener("click", () => showThread(message.thread_id));
|
|
163
|
+
container.appendChild(threadButton);
|
|
164
|
+
|
|
165
|
+
container.appendChild(node("div", "body", message.body));
|
|
166
|
+
|
|
167
|
+
const deliveries = node("div", "deliveries");
|
|
168
|
+
deliveries.appendChild(node("h2", null, "Distribution"));
|
|
169
|
+
const list = node("ul");
|
|
170
|
+
for (const delivery of message.deliveries) {
|
|
171
|
+
list.appendChild(
|
|
172
|
+
node(
|
|
173
|
+
"li",
|
|
174
|
+
null,
|
|
175
|
+
delivery.agent_id +
|
|
176
|
+
" · " +
|
|
177
|
+
delivery.role +
|
|
178
|
+
" · dossier " +
|
|
179
|
+
delivery.folder +
|
|
180
|
+
" · " +
|
|
181
|
+
(delivery.read ? "lu" : "non lu")
|
|
182
|
+
)
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
deliveries.appendChild(list);
|
|
186
|
+
container.appendChild(deliveries);
|
|
187
|
+
|
|
188
|
+
setDetail(container, false);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async function showThread(threadId) {
|
|
192
|
+
try {
|
|
193
|
+
const data = await api("/admin/threads/" + encodeURIComponent(threadId));
|
|
194
|
+
const container = node("div", "detail");
|
|
195
|
+
container.appendChild(node("h3", null, "Fil " + threadId + " — " + data.messages.length + " message(s)"));
|
|
196
|
+
|
|
197
|
+
const filterButton = node("button", null, "Filtrer la liste sur ce fil");
|
|
198
|
+
filterButton.addEventListener("click", () => {
|
|
199
|
+
state.threadId = threadId;
|
|
200
|
+
refresh({ keepDetail: true });
|
|
201
|
+
});
|
|
202
|
+
container.appendChild(filterButton);
|
|
203
|
+
|
|
204
|
+
for (const message of data.messages) {
|
|
205
|
+
const block = node("div", "thread-message");
|
|
206
|
+
block.appendChild(node("div", "subject", message.subject));
|
|
207
|
+
block.appendChild(
|
|
208
|
+
node("div", "meta", message.from + " → " + (message.to.join(", ") || "—") + " · " + formatDate(message.created_at))
|
|
209
|
+
);
|
|
210
|
+
block.appendChild(node("div", "preview", message.preview));
|
|
211
|
+
const openButton = node("button", "link", "ouvrir");
|
|
212
|
+
openButton.addEventListener("click", () => openMessage(message.message_id));
|
|
213
|
+
block.appendChild(openButton);
|
|
214
|
+
container.appendChild(block);
|
|
215
|
+
}
|
|
216
|
+
setDetail(container, false);
|
|
217
|
+
} catch (error) {
|
|
218
|
+
setDetail(node("div", "error", error.message), false);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async function refresh(options) {
|
|
223
|
+
try {
|
|
224
|
+
const [overview, list] = await Promise.all([
|
|
225
|
+
api("/admin/overview"),
|
|
226
|
+
api("/admin/messages", { agent: state.agent, search: state.search, thread_id: state.threadId, limit: 100 }),
|
|
227
|
+
]);
|
|
228
|
+
el.tokenBar.hidden = true;
|
|
229
|
+
renderAgents(overview);
|
|
230
|
+
renderMessages(list.messages);
|
|
231
|
+
if (!options || !options.keepDetail) {
|
|
232
|
+
if (!state.selectedMessageId) {
|
|
233
|
+
setDetail(node("div", null, "Sélectionnez un message pour voir son contenu."), true);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
} catch (error) {
|
|
237
|
+
el.statusDot.className = "dot ko";
|
|
238
|
+
if (error.message === "unauthorized") {
|
|
239
|
+
el.serverStats.textContent = "authentification requise";
|
|
240
|
+
} else {
|
|
241
|
+
el.serverStats.textContent = "erreur : " + error.message;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
el.refresh.addEventListener("click", () => refresh({ keepDetail: true }));
|
|
247
|
+
el.clearFilters.addEventListener("click", () => {
|
|
248
|
+
state.agent = null;
|
|
249
|
+
state.threadId = null;
|
|
250
|
+
state.search = "";
|
|
251
|
+
el.search.value = "";
|
|
252
|
+
refresh({ keepDetail: true });
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
let searchTimer = null;
|
|
256
|
+
el.search.addEventListener("input", (event) => {
|
|
257
|
+
state.search = event.target.value.trim();
|
|
258
|
+
clearTimeout(searchTimer);
|
|
259
|
+
searchTimer = setTimeout(() => refresh({ keepDetail: true }), 250);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
el.saveToken.addEventListener("click", () => {
|
|
263
|
+
state.token = el.tokenInput.value.trim();
|
|
264
|
+
localStorage.setItem(TOKEN_KEY, state.token);
|
|
265
|
+
el.tokenError.textContent = "";
|
|
266
|
+
refresh({ keepDetail: true }).then(() => {
|
|
267
|
+
if (!el.tokenBar.hidden) el.tokenError.textContent = "jeton refusé";
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
setInterval(() => {
|
|
272
|
+
if (el.autoRefresh.checked) refresh({ keepDetail: true });
|
|
273
|
+
}, REFRESH_MS);
|
|
274
|
+
|
|
275
|
+
refresh();
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="fr">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>MCP Messenger — supervision</title>
|
|
7
|
+
<link rel="stylesheet" href="./styles.css" />
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<header class="topbar">
|
|
11
|
+
<div class="brand">
|
|
12
|
+
<span class="dot" id="status-dot"></span>
|
|
13
|
+
<h1>MCP Messenger</h1>
|
|
14
|
+
<span class="muted" id="server-stats">chargement…</span>
|
|
15
|
+
</div>
|
|
16
|
+
<div class="controls">
|
|
17
|
+
<label class="auto">
|
|
18
|
+
<input type="checkbox" id="auto-refresh" checked />
|
|
19
|
+
rafraîchissement auto
|
|
20
|
+
</label>
|
|
21
|
+
<button id="refresh">Rafraîchir</button>
|
|
22
|
+
</div>
|
|
23
|
+
</header>
|
|
24
|
+
|
|
25
|
+
<div id="token-bar" class="token-bar" hidden>
|
|
26
|
+
<label for="token">Jeton d'accès requis</label>
|
|
27
|
+
<input type="password" id="token" placeholder="MESSENGER_ADMIN_TOKEN ou MESSENGER_TOKEN" />
|
|
28
|
+
<button id="save-token">Se connecter</button>
|
|
29
|
+
<span class="error" id="token-error"></span>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<main>
|
|
33
|
+
<aside class="panel agents">
|
|
34
|
+
<h2>Agents</h2>
|
|
35
|
+
<ul id="agents"></ul>
|
|
36
|
+
</aside>
|
|
37
|
+
|
|
38
|
+
<section class="panel messages">
|
|
39
|
+
<div class="filters">
|
|
40
|
+
<input type="search" id="search" placeholder="Rechercher dans les sujets et les corps…" />
|
|
41
|
+
<span class="muted" id="filter-label"></span>
|
|
42
|
+
<button id="clear-filters" class="link">tout afficher</button>
|
|
43
|
+
</div>
|
|
44
|
+
<ul id="messages"></ul>
|
|
45
|
+
</section>
|
|
46
|
+
|
|
47
|
+
<section class="panel detail">
|
|
48
|
+
<div id="detail" class="empty">Sélectionnez un message pour voir son contenu.</div>
|
|
49
|
+
</section>
|
|
50
|
+
</main>
|
|
51
|
+
<script type="module" src="./app.js"></script>
|
|
52
|
+
</body>
|
|
53
|
+
</html>
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
--bg: #f6f7f9;
|
|
3
|
+
--panel: #ffffff;
|
|
4
|
+
--border: #e2e5ea;
|
|
5
|
+
--text: #1c2024;
|
|
6
|
+
--muted: #6b7280;
|
|
7
|
+
--accent: #2f6feb;
|
|
8
|
+
--accent-soft: #e8f0fe;
|
|
9
|
+
--danger: #c8362f;
|
|
10
|
+
--radius: 8px;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
@media (prefers-color-scheme: dark) {
|
|
14
|
+
:root {
|
|
15
|
+
--bg: #14171c;
|
|
16
|
+
--panel: #1b1f26;
|
|
17
|
+
--border: #2b313b;
|
|
18
|
+
--text: #e6e9ee;
|
|
19
|
+
--muted: #98a1b0;
|
|
20
|
+
--accent: #7aa2f7;
|
|
21
|
+
--accent-soft: #223049;
|
|
22
|
+
--danger: #f07178;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
* {
|
|
27
|
+
box-sizing: border-box;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/* `hidden` doit l'emporter sur les `display` posés plus bas (barre de jeton). */
|
|
31
|
+
[hidden] {
|
|
32
|
+
display: none !important;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
body {
|
|
36
|
+
margin: 0;
|
|
37
|
+
font: 14px/1.5 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
38
|
+
background: var(--bg);
|
|
39
|
+
color: var(--text);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.topbar {
|
|
43
|
+
display: flex;
|
|
44
|
+
align-items: center;
|
|
45
|
+
justify-content: space-between;
|
|
46
|
+
gap: 16px;
|
|
47
|
+
padding: 12px 20px;
|
|
48
|
+
background: var(--panel);
|
|
49
|
+
border-bottom: 1px solid var(--border);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.brand {
|
|
53
|
+
display: flex;
|
|
54
|
+
align-items: baseline;
|
|
55
|
+
gap: 12px;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.brand h1 {
|
|
59
|
+
font-size: 16px;
|
|
60
|
+
margin: 0;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.dot {
|
|
64
|
+
width: 9px;
|
|
65
|
+
height: 9px;
|
|
66
|
+
border-radius: 50%;
|
|
67
|
+
background: var(--muted);
|
|
68
|
+
align-self: center;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.dot.ok {
|
|
72
|
+
background: #2ea043;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.dot.ko {
|
|
76
|
+
background: var(--danger);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.controls {
|
|
80
|
+
display: flex;
|
|
81
|
+
align-items: center;
|
|
82
|
+
gap: 12px;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.auto {
|
|
86
|
+
color: var(--muted);
|
|
87
|
+
display: flex;
|
|
88
|
+
align-items: center;
|
|
89
|
+
gap: 6px;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
button {
|
|
93
|
+
font: inherit;
|
|
94
|
+
padding: 6px 12px;
|
|
95
|
+
border-radius: var(--radius);
|
|
96
|
+
border: 1px solid var(--border);
|
|
97
|
+
background: var(--panel);
|
|
98
|
+
color: var(--text);
|
|
99
|
+
cursor: pointer;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
button:hover {
|
|
103
|
+
border-color: var(--accent);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
button.link {
|
|
107
|
+
border: none;
|
|
108
|
+
background: none;
|
|
109
|
+
color: var(--accent);
|
|
110
|
+
padding: 0;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
input[type="search"],
|
|
114
|
+
input[type="password"] {
|
|
115
|
+
font: inherit;
|
|
116
|
+
padding: 6px 10px;
|
|
117
|
+
border-radius: var(--radius);
|
|
118
|
+
border: 1px solid var(--border);
|
|
119
|
+
background: var(--panel);
|
|
120
|
+
color: var(--text);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.token-bar {
|
|
124
|
+
display: flex;
|
|
125
|
+
align-items: center;
|
|
126
|
+
gap: 10px;
|
|
127
|
+
padding: 12px 20px;
|
|
128
|
+
background: var(--accent-soft);
|
|
129
|
+
border-bottom: 1px solid var(--border);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.token-bar input {
|
|
133
|
+
min-width: 320px;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
main {
|
|
137
|
+
display: grid;
|
|
138
|
+
grid-template-columns: 260px minmax(320px, 1fr) minmax(320px, 1.2fr);
|
|
139
|
+
gap: 12px;
|
|
140
|
+
padding: 12px;
|
|
141
|
+
align-items: start;
|
|
142
|
+
height: calc(100vh - 58px);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.panel {
|
|
146
|
+
background: var(--panel);
|
|
147
|
+
border: 1px solid var(--border);
|
|
148
|
+
border-radius: var(--radius);
|
|
149
|
+
height: 100%;
|
|
150
|
+
overflow: auto;
|
|
151
|
+
padding: 12px;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.panel h2 {
|
|
155
|
+
font-size: 12px;
|
|
156
|
+
text-transform: uppercase;
|
|
157
|
+
letter-spacing: 0.06em;
|
|
158
|
+
color: var(--muted);
|
|
159
|
+
margin: 0 0 8px;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
ul {
|
|
163
|
+
list-style: none;
|
|
164
|
+
margin: 0;
|
|
165
|
+
padding: 0;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.agents li {
|
|
169
|
+
padding: 8px;
|
|
170
|
+
border-radius: var(--radius);
|
|
171
|
+
cursor: pointer;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.agents li:hover {
|
|
175
|
+
background: var(--accent-soft);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.agents li.selected {
|
|
179
|
+
background: var(--accent-soft);
|
|
180
|
+
outline: 1px solid var(--accent);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.agents .id {
|
|
184
|
+
font-weight: 600;
|
|
185
|
+
word-break: break-all;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.filters {
|
|
189
|
+
display: flex;
|
|
190
|
+
align-items: center;
|
|
191
|
+
gap: 10px;
|
|
192
|
+
margin-bottom: 10px;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.filters input {
|
|
196
|
+
flex: 1;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
.messages li {
|
|
200
|
+
padding: 10px;
|
|
201
|
+
border-radius: var(--radius);
|
|
202
|
+
border: 1px solid transparent;
|
|
203
|
+
cursor: pointer;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.messages li:hover {
|
|
207
|
+
background: var(--accent-soft);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
.messages li.selected {
|
|
211
|
+
border-color: var(--accent);
|
|
212
|
+
background: var(--accent-soft);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.messages .subject {
|
|
216
|
+
font-weight: 600;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
.messages .route {
|
|
220
|
+
color: var(--muted);
|
|
221
|
+
font-size: 12px;
|
|
222
|
+
word-break: break-all;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
.messages .preview {
|
|
226
|
+
color: var(--muted);
|
|
227
|
+
margin-top: 4px;
|
|
228
|
+
display: -webkit-box;
|
|
229
|
+
-webkit-line-clamp: 2;
|
|
230
|
+
-webkit-box-orient: vertical;
|
|
231
|
+
overflow: hidden;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
.muted {
|
|
235
|
+
color: var(--muted);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.error {
|
|
239
|
+
color: var(--danger);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.badge {
|
|
243
|
+
display: inline-block;
|
|
244
|
+
font-size: 11px;
|
|
245
|
+
padding: 1px 6px;
|
|
246
|
+
border-radius: 999px;
|
|
247
|
+
border: 1px solid var(--border);
|
|
248
|
+
color: var(--muted);
|
|
249
|
+
margin-left: 6px;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.badge.unread {
|
|
253
|
+
border-color: var(--accent);
|
|
254
|
+
color: var(--accent);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
.badge.high {
|
|
258
|
+
border-color: var(--danger);
|
|
259
|
+
color: var(--danger);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
.detail h3 {
|
|
263
|
+
margin: 0 0 4px;
|
|
264
|
+
font-size: 16px;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
.detail .meta {
|
|
268
|
+
color: var(--muted);
|
|
269
|
+
font-size: 12px;
|
|
270
|
+
white-space: pre-line;
|
|
271
|
+
margin-bottom: 12px;
|
|
272
|
+
word-break: break-all;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
.detail .body {
|
|
276
|
+
white-space: pre-wrap;
|
|
277
|
+
word-wrap: break-word;
|
|
278
|
+
border-top: 1px solid var(--border);
|
|
279
|
+
padding-top: 12px;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
.detail .deliveries {
|
|
283
|
+
margin-top: 16px;
|
|
284
|
+
border-top: 1px solid var(--border);
|
|
285
|
+
padding-top: 12px;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.detail .deliveries li {
|
|
289
|
+
padding: 2px 0;
|
|
290
|
+
color: var(--muted);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
.thread-message {
|
|
294
|
+
border-top: 1px solid var(--border);
|
|
295
|
+
padding-top: 10px;
|
|
296
|
+
margin-top: 10px;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
.empty {
|
|
300
|
+
color: var(--muted);
|
|
301
|
+
padding: 24px 8px;
|
|
302
|
+
text-align: center;
|
|
303
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const APP_VERSION = "1.0.1";
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@imenam/mcp-messenger",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"main": "dist/src/index.js",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"test": "npm run build && vitest run",
|
|
7
|
+
"build": "node scripts/generate-version.js && tsc && node scripts/copy-assets.js",
|
|
8
|
+
"start": "node dist/src/index.js",
|
|
9
|
+
"serve": "node dist/src/index.js --server",
|
|
10
|
+
"prepublishOnly": "npm run build",
|
|
11
|
+
"release": "npm version patch && npm publish && git push --follow-tags",
|
|
12
|
+
"typecheck": "tsc --noEmit",
|
|
13
|
+
"test:watch": "vitest",
|
|
14
|
+
"test:coverage": "vitest run --coverage"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"mcp",
|
|
18
|
+
"model-context-protocol",
|
|
19
|
+
"agent",
|
|
20
|
+
"messaging",
|
|
21
|
+
"mailbox",
|
|
22
|
+
"agent-to-agent"
|
|
23
|
+
],
|
|
24
|
+
"author": "Matthieu Pesnot-Pin <matthieupin@gmail.com>",
|
|
25
|
+
"license": "ISC",
|
|
26
|
+
"description": "Cross-platform agent-to-agent messaging: an MCP mailbox client plus its HTTP mailbox server.",
|
|
27
|
+
"type": "module",
|
|
28
|
+
"bin": {
|
|
29
|
+
"mcp-messenger": "dist/src/index.js"
|
|
30
|
+
},
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=20"
|
|
33
|
+
},
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
39
|
+
"better-sqlite3": "^12.11.1",
|
|
40
|
+
"dotenv": "^17.4.2",
|
|
41
|
+
"express": "^5.2.1",
|
|
42
|
+
"express-rate-limit": "^8.7.0",
|
|
43
|
+
"helmet": "^8.3.0",
|
|
44
|
+
"zod": "^4.5.4"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
48
|
+
"@types/express": "^5.0.6",
|
|
49
|
+
"@types/node": "^22.20.1",
|
|
50
|
+
"typescript": "^5.9.3",
|
|
51
|
+
"vitest": "^3.2.7"
|
|
52
|
+
},
|
|
53
|
+
"files": [
|
|
54
|
+
"dist/src",
|
|
55
|
+
"README.md",
|
|
56
|
+
".env.example",
|
|
57
|
+
"LICENSE"
|
|
58
|
+
]
|
|
59
|
+
}
|