agentschat-mcp 0.30.0 → 0.32.0

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.
@@ -0,0 +1,459 @@
1
+ #!/usr/bin/env node
2
+ // connector/run.ts
3
+ import WS from "ws";
4
+
5
+ // connector/server.ts
6
+ import { createServer } from "node:http";
7
+ import { WebSocketServer, WebSocket } from "ws";
8
+
9
+ // connector/auth.ts
10
+ import { createHmac, timingSafeEqual } from "node:crypto";
11
+ var CLOSE_UNAUTHORIZED = 4401;
12
+ function hmacHex(payload, secret) {
13
+ return createHmac("sha256", secret).update(payload, "utf8").digest("hex");
14
+ }
15
+ function verifySignature(payload, sigHex, secrets) {
16
+ let sigBuf;
17
+ try {
18
+ sigBuf = Buffer.from(sigHex, "hex");
19
+ } catch {
20
+ return false;
21
+ }
22
+ if (sigBuf.length === 0)
23
+ return false;
24
+ for (const secret of secrets) {
25
+ if (!secret)
26
+ continue;
27
+ const expected = Buffer.from(hmacHex(payload, secret), "hex");
28
+ if (expected.length !== sigBuf.length)
29
+ continue;
30
+ if (timingSafeEqual(sigBuf, expected))
31
+ return true;
32
+ }
33
+ return false;
34
+ }
35
+ function verifyToken(token, secrets) {
36
+ let decoded;
37
+ try {
38
+ decoded = Buffer.from(token, "base64url").toString("utf8");
39
+ } catch {
40
+ return null;
41
+ }
42
+ const parts = decoded.split(":");
43
+ if (parts.length < 3)
44
+ return null;
45
+ const sig = parts[parts.length - 1];
46
+ const exp = Number.parseInt(parts[parts.length - 2], 10);
47
+ if (!Number.isFinite(exp))
48
+ return null;
49
+ const payload = parts.slice(0, -2).join(":");
50
+ if (exp !== 0 && Math.floor(Date.now() / 1000) > exp)
51
+ return null;
52
+ const signed = `${payload}:${exp}`;
53
+ return verifySignature(signed, sig, secrets) ? payload : null;
54
+ }
55
+ function verifyUpgradeToken(token, secrets) {
56
+ return verifyToken(token, secrets);
57
+ }
58
+
59
+ // connector/descriptor.ts
60
+ var CONTRACT_VERSION = 1;
61
+ var AGENTSCHAT_MAX_MESSAGE_LENGTH = 4000;
62
+ var SUPPORTED_OPS = ["send", "typing", "get_chat_info"];
63
+ function buildDescriptor(overrides = {}) {
64
+ return {
65
+ contract_version: CONTRACT_VERSION,
66
+ platform: "agentschat",
67
+ label: "AgentsChat",
68
+ max_message_length: AGENTSCHAT_MAX_MESSAGE_LENGTH,
69
+ supports_draft_streaming: false,
70
+ supports_edit: false,
71
+ supports_threads: false,
72
+ markdown_dialect: "markdown",
73
+ len_unit: "chars",
74
+ emoji: "\uD83E\uDD16",
75
+ pii_safe: false,
76
+ supported_ops: [...SUPPORTED_OPS],
77
+ ...overrides
78
+ };
79
+ }
80
+
81
+ // connector/normalize.ts
82
+ function toWireEvent(msg, platform = "agentschat") {
83
+ const content = msg.content ?? "";
84
+ if (content === "__typing__")
85
+ return null;
86
+ const chatId = msg.channel_id ?? "";
87
+ if (!chatId)
88
+ return null;
89
+ const isDm = chatId.startsWith("dm-");
90
+ return {
91
+ text: content,
92
+ message_type: "text",
93
+ message_id: msg.id,
94
+ reply_to_message_id: msg.reply_to,
95
+ source: {
96
+ platform,
97
+ chat_id: chatId,
98
+ chat_type: isDm ? "dm" : "group",
99
+ chat_name: msg.channel_id ?? null,
100
+ user_id: msg.sender_id,
101
+ user_name: msg.sender_name ?? msg.sender_id,
102
+ thread_id: null
103
+ }
104
+ };
105
+ }
106
+
107
+ // connector/identities.ts
108
+ class IdentityTable {
109
+ byBot = new Map;
110
+ constructor(identities) {
111
+ for (const id of identities) {
112
+ if (this.byBot.has(id.botId)) {
113
+ throw new Error(`duplicate identity botId "${id.botId}" — ambiguous routing`);
114
+ }
115
+ this.byBot.set(id.botId, id);
116
+ }
117
+ }
118
+ forBot(botId) {
119
+ return this.byBot.get(botId) ?? null;
120
+ }
121
+ isSingle() {
122
+ return this.byBot.size === 1;
123
+ }
124
+ get size() {
125
+ return this.byBot.size;
126
+ }
127
+ all() {
128
+ return [...this.byBot.values()];
129
+ }
130
+ }
131
+ function routeInbound(table, ctx) {
132
+ if (ctx.channel_id?.startsWith("dm-")) {
133
+ return ctx.dmOwnerBotId ? table.forBot(ctx.dmOwnerBotId) : null;
134
+ }
135
+ const mentioned = Array.isArray(ctx.mentioned_ids) ? ctx.mentioned_ids : [];
136
+ for (const mid of mentioned) {
137
+ const id = table.forBot(mid);
138
+ if (id)
139
+ return id;
140
+ }
141
+ return null;
142
+ }
143
+
144
+ // connector/server.ts
145
+ function startConnector(config) {
146
+ const log = config.logger ?? (() => {});
147
+ const descriptor = buildDescriptor(config.descriptor);
148
+ const single = !config.identities || config.identities.length === 0;
149
+ const table = new IdentityTable(single ? [{ botId: "default", agentId: "default", token: "", gatewayId: "", secret: "" }] : config.identities);
150
+ const hooks = single ? {
151
+ sendMessage: (_b, chatId, content, replyTo) => config.agentschat.sendMessage(chatId, content, replyTo),
152
+ getChatInfo: (_b, chatId) => config.agentschat.getChatInfo(chatId),
153
+ sendTyping: (_b, chatId) => config.agentschat.sendTyping?.(chatId) ?? Promise.resolve()
154
+ } : config.agentschat;
155
+ const sockets = new Set;
156
+ const http = createServer((_req, res) => {
157
+ res.writeHead(200, { "Content-Type": "application/json" });
158
+ res.end(JSON.stringify({ ok: true, service: "agentschat-connector", contract_version: 1, identities: table.size }));
159
+ });
160
+ const wss = new WebSocketServer({ noServer: true });
161
+ http.on("upgrade", (req, socket, head) => {
162
+ const { pathname } = new URL(req.url ?? "/", "http://localhost");
163
+ if (pathname !== "/relay") {
164
+ socket.destroy();
165
+ return;
166
+ }
167
+ const auth = req.headers.authorization ?? "";
168
+ const token = auth.startsWith("Bearer ") ? auth.slice(7).trim() : "";
169
+ const payload = peekPayload(token);
170
+ const secrets = payload ? config.secrets[payload] : undefined;
171
+ const gatewayId = secrets ? verifyUpgradeToken(token, secrets) : null;
172
+ if (!gatewayId) {
173
+ log(`[connector] rejecting upgrade: bad/absent token (path=${pathname})`);
174
+ wss.handleUpgrade(req, socket, head, (ws) => {
175
+ ws.close(CLOSE_UNAUTHORIZED, "unauthorized");
176
+ });
177
+ return;
178
+ }
179
+ wss.handleUpgrade(req, socket, head, (ws) => {
180
+ onConnection(ws, gatewayId);
181
+ });
182
+ });
183
+ function onConnection(ws, gatewayId) {
184
+ const conn = { ws, gatewayId, fronted: new Set };
185
+ sockets.add(conn);
186
+ log(`[connector] gateway connected: ${gatewayId} (${sockets.size} total)`);
187
+ ws.on("message", async (data) => {
188
+ const text = data.toString();
189
+ for (const line of text.split(`
190
+ `)) {
191
+ if (!line.trim())
192
+ continue;
193
+ let frame;
194
+ try {
195
+ frame = JSON.parse(line);
196
+ } catch {
197
+ continue;
198
+ }
199
+ await handleFrame(conn, frame).catch((e) => log(`[connector] frame error: ${e}`));
200
+ }
201
+ });
202
+ ws.on("close", () => {
203
+ sockets.delete(conn);
204
+ log(`[connector] gateway disconnected: ${gatewayId} (${sockets.size} left)`);
205
+ });
206
+ ws.on("error", () => sockets.delete(conn));
207
+ }
208
+ async function handleFrame(conn, frame) {
209
+ const t = frame?.type;
210
+ if (t === "hello") {
211
+ const botId = String(frame.botId ?? "");
212
+ const identity = single ? table.all()[0] : botId ? table.forBot(botId) : null;
213
+ if (!identity) {
214
+ send(conn.ws, { type: "error", error: `unknown identity botId: ${botId || "(none)"}` });
215
+ return;
216
+ }
217
+ conn.fronted.add(identity.botId);
218
+ send(conn.ws, { type: "descriptor", descriptor: { ...descriptor, platform: "agentschat" } });
219
+ return;
220
+ }
221
+ if (t === "outbound") {
222
+ const result = await handleOutbound(conn, frame);
223
+ send(conn.ws, { type: "outbound_result", requestId: frame.requestId, result });
224
+ return;
225
+ }
226
+ }
227
+ async function handleOutbound(conn, frame) {
228
+ const action = frame?.action ?? {};
229
+ const op = action?.op;
230
+ const chatId = action?.chat_id ?? "";
231
+ const firstFronted = [...conn.fronted][0];
232
+ const requested = typeof frame?.botId === "string" && frame.botId ? frame.botId : firstFronted ?? null;
233
+ const identity = single ? table.all()[0] : requested && conn.fronted.has(requested) ? table.forBot(requested) : null;
234
+ if (!identity) {
235
+ log(`[connector] outbound failed: no fronted identity for botId=${requested ?? "?"}`);
236
+ return { success: false, error: `no fronted identity for outbound (botId=${requested ?? "?"})` };
237
+ }
238
+ switch (op) {
239
+ case "send": {
240
+ const r = await hooks.sendMessage(identity.botId, chatId, action.content ?? "", action.reply_to);
241
+ return { success: true, message_id: r?.id };
242
+ }
243
+ case "typing": {
244
+ await hooks.sendTyping?.(identity.botId, chatId);
245
+ return { success: true };
246
+ }
247
+ case "get_chat_info": {
248
+ const info = await hooks.getChatInfo(identity.botId, chatId);
249
+ return info ?? {};
250
+ }
251
+ default:
252
+ log(`[connector] outbound failed: unsupported op ${String(op)}`);
253
+ return { success: false, error: `unsupported op: ${String(op)}` };
254
+ }
255
+ }
256
+ function send(ws, obj) {
257
+ if (ws.readyState === WebSocket.OPEN)
258
+ ws.send(JSON.stringify(obj) + `
259
+ `);
260
+ }
261
+ http.listen(config.port, config.host ?? "127.0.0.1");
262
+ const address = http.address();
263
+ const port = typeof address === "object" && address ? address.port : config.port;
264
+ return {
265
+ port,
266
+ stop() {
267
+ for (const c of sockets) {
268
+ try {
269
+ c.ws.close(1001, "connector shutdown");
270
+ } catch {}
271
+ }
272
+ wss.close();
273
+ http.close();
274
+ },
275
+ injectAgentsChatMessage(msg) {
276
+ const bySocket = msg.__botId ? table.forBot(String(msg.__botId)) : null;
277
+ const target = bySocket ?? routeInbound(table, {
278
+ channel_id: msg.channel_id,
279
+ mentioned_ids: msg.mentioned_ids,
280
+ dmOwnerBotId: msg.dm_owner
281
+ }) ?? (table.isSingle() ? table.all()[0] : null);
282
+ if (!target) {
283
+ log(`[connector] inbound unaddressed (channel=${msg.channel_id ?? "?"} mentions=${JSON.stringify(msg.mentioned_ids ?? [])}) — dropped`);
284
+ return;
285
+ }
286
+ const event = toWireEvent(msg, "agentschat");
287
+ if (!event)
288
+ return;
289
+ event.source.profile = target.botId;
290
+ for (const conn of sockets) {
291
+ if (conn.fronted.has(target.botId))
292
+ send(conn.ws, { type: "inbound", event });
293
+ }
294
+ },
295
+ connections() {
296
+ return sockets.size;
297
+ }
298
+ };
299
+ }
300
+ function peekPayload(token) {
301
+ try {
302
+ const decoded = Buffer.from(token, "base64url").toString("utf8");
303
+ const parts = decoded.split(":");
304
+ if (parts.length < 3)
305
+ return null;
306
+ return parts.slice(0, -2).join(":");
307
+ } catch {
308
+ return null;
309
+ }
310
+ }
311
+
312
+ // connector/run.ts
313
+ var log = (m) => process.stderr.write(`[agentschat-connector] ${m}
314
+ `);
315
+ function need(name) {
316
+ const v = process.env[name];
317
+ if (!v) {
318
+ log(`ERROR: ${name} is required`);
319
+ process.exit(1);
320
+ }
321
+ return v;
322
+ }
323
+ var API = (process.env.AGENTCHAT_API_URL || "https://agents-chat.com").replace(/\/$/, "");
324
+ var WS_URL = process.env.AGENTCHAT_WS_URL || API.replace(/^http/, "ws") + "/ws";
325
+ var PORT = Number(process.env.RELAY_PORT || 8765);
326
+ var HOST = process.env.RELAY_HOST || "127.0.0.1";
327
+ function resolveIdentities() {
328
+ const raw = (process.env.RELAY_IDENTITIES || "").trim();
329
+ if (raw) {
330
+ let parsed;
331
+ try {
332
+ parsed = JSON.parse(raw);
333
+ } catch (e) {
334
+ log(`ERROR: RELAY_IDENTITIES is not valid JSON: ${e}`);
335
+ process.exit(1);
336
+ }
337
+ if (!Array.isArray(parsed) || parsed.length === 0) {
338
+ log(`ERROR: RELAY_IDENTITIES must be a non-empty JSON array`);
339
+ process.exit(1);
340
+ }
341
+ for (const it of parsed) {
342
+ if (!it?.botId || !it?.token || !it?.gatewayId || !it?.secret) {
343
+ log(`ERROR: each RELAY_IDENTITIES entry needs botId, token, gatewayId, secret — got: ${JSON.stringify(it).slice(0, 80)}`);
344
+ process.exit(1);
345
+ }
346
+ }
347
+ return parsed.map((it) => ({
348
+ botId: String(it.botId),
349
+ agentId: String(it.agentId ?? it.botId),
350
+ token: String(it.token),
351
+ gatewayId: String(it.gatewayId),
352
+ secret: String(it.secret)
353
+ }));
354
+ }
355
+ const agentId = need("AGENTCHAT_AGENT_ID");
356
+ const token = need("AGENTCHAT_TOKEN");
357
+ const gatewayId = need("RELAY_GATEWAY_ID");
358
+ const secret = need("RELAY_GATEWAY_SECRET");
359
+ return [{ botId: agentId, agentId, token, gatewayId, secret }];
360
+ }
361
+ var identities = resolveIdentities();
362
+ var single = identities.length === 1;
363
+ var secrets = {};
364
+ for (const id of identities) {
365
+ (secrets[id.gatewayId] ??= []).push(id.secret);
366
+ }
367
+ var broadcast = null;
368
+ var socketsByBot = new Map;
369
+ var backoffByBot = new Map;
370
+ function connectIdentity(id) {
371
+ const ws = new WS(WS_URL);
372
+ socketsByBot.set(id.botId, ws);
373
+ ws.on("open", () => {
374
+ backoffByBot.set(id.botId, 1000);
375
+ try {
376
+ ws.send(JSON.stringify({ type: "auth", agent_id: id.agentId, token: id.token, capabilities: ["chat"] }));
377
+ } catch {}
378
+ });
379
+ ws.on("message", (raw) => {
380
+ let data;
381
+ try {
382
+ data = JSON.parse(String(raw));
383
+ } catch {
384
+ return;
385
+ }
386
+ if (data.type === "auth_ok") {
387
+ log(`connected to agentschat as ${id.agentId}`);
388
+ return;
389
+ }
390
+ if (data.type === "message" && data.sender_id !== id.agentId) {
391
+ broadcast?.({ ...data, __botId: id.botId });
392
+ }
393
+ });
394
+ ws.on("close", () => {
395
+ if (process.__shutdown)
396
+ return;
397
+ const delay = backoffByBot.get(id.botId) ?? 1000;
398
+ log(`agentschat WS closed for ${id.botId}; reconnecting in ${delay}ms`);
399
+ setTimeout(() => connectIdentity(id), delay);
400
+ backoffByBot.set(id.botId, Math.min(delay * 2, 30000));
401
+ });
402
+ ws.on("error", (e) => log(`agentschat WS error (${id.botId}): ${e?.message ?? e}`));
403
+ }
404
+ var connector = startConnector({
405
+ port: PORT,
406
+ host: HOST,
407
+ secrets,
408
+ identities: single ? undefined : identities,
409
+ agentschat: {
410
+ async sendMessage(botId, chatId, content, replyTo) {
411
+ const id = identities.find((i) => i.botId === botId) ?? identities[0];
412
+ const res = await fetch(`${API}/api/channels/${encodeURIComponent(chatId)}/messages`, {
413
+ method: "POST",
414
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${id.token}` },
415
+ body: JSON.stringify({ sender_id: id.agentId, content_type: "text", content, ...replyTo ? { parent_id: replyTo } : {} })
416
+ });
417
+ if (!res.ok)
418
+ throw new Error(`agentschat send failed: ${res.status}`);
419
+ const data = await res.json();
420
+ return { id: data?.id };
421
+ },
422
+ async getChatInfo(botId, chatId) {
423
+ const id = identities.find((i) => i.botId === botId) ?? identities[0];
424
+ const res = await fetch(`${API}/api/channels/${encodeURIComponent(chatId)}`, {
425
+ headers: { Authorization: `Bearer ${id.token}` }
426
+ });
427
+ if (!res.ok)
428
+ return { name: chatId, type: chatId.startsWith("dm-") ? "dm" : "group" };
429
+ const data = await res.json();
430
+ return { name: data?.name ?? chatId, type: chatId.startsWith("dm-") ? "dm" : "group" };
431
+ },
432
+ async sendTyping(botId, chatId) {
433
+ const id = identities.find((i) => i.botId === botId) ?? identities[0];
434
+ const ws = socketsByBot.get(id.botId);
435
+ if (ws && ws.readyState === WS.OPEN) {
436
+ try {
437
+ ws.send(JSON.stringify({ type: "typing", channel_id: chatId, sender_id: id.agentId }));
438
+ } catch {}
439
+ }
440
+ }
441
+ },
442
+ logger: log
443
+ });
444
+ broadcast = (msg) => connector.injectAgentsChatMessage(msg);
445
+ log(`listening on ${HOST}:${connector.port} (contract v1, ${identities.length} identit${identities.length === 1 ? "y" : "ies"})`);
446
+ for (const id of identities)
447
+ connectIdentity(id);
448
+ for (const sig of ["SIGINT", "SIGTERM"]) {
449
+ process.on(sig, () => {
450
+ process.__shutdown = true;
451
+ for (const ws of socketsByBot.values()) {
452
+ try {
453
+ ws.close();
454
+ } catch {}
455
+ }
456
+ connector.stop();
457
+ process.exit(0);
458
+ });
459
+ }