agentschat-mcp 0.29.1 → 0.31.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,334 @@
1
+ #!/usr/bin/env node
2
+ // connector/server.ts
3
+ import { createServer } from "node:http";
4
+ import { WebSocketServer, WebSocket } from "ws";
5
+
6
+ // connector/auth.ts
7
+ import { createHmac, timingSafeEqual } from "node:crypto";
8
+ var CLOSE_UNAUTHORIZED = 4401;
9
+ function hmacHex(payload, secret) {
10
+ return createHmac("sha256", secret).update(payload, "utf8").digest("hex");
11
+ }
12
+ function verifySignature(payload, sigHex, secrets) {
13
+ let sigBuf;
14
+ try {
15
+ sigBuf = Buffer.from(sigHex, "hex");
16
+ } catch {
17
+ return false;
18
+ }
19
+ if (sigBuf.length === 0)
20
+ return false;
21
+ for (const secret of secrets) {
22
+ if (!secret)
23
+ continue;
24
+ const expected = Buffer.from(hmacHex(payload, secret), "hex");
25
+ if (expected.length !== sigBuf.length)
26
+ continue;
27
+ if (timingSafeEqual(sigBuf, expected))
28
+ return true;
29
+ }
30
+ return false;
31
+ }
32
+ function verifyToken(token, secrets) {
33
+ let decoded;
34
+ try {
35
+ decoded = Buffer.from(token, "base64url").toString("utf8");
36
+ } catch {
37
+ return null;
38
+ }
39
+ const parts = decoded.split(":");
40
+ if (parts.length < 3)
41
+ return null;
42
+ const sig = parts[parts.length - 1];
43
+ const exp = Number.parseInt(parts[parts.length - 2], 10);
44
+ if (!Number.isFinite(exp))
45
+ return null;
46
+ const payload = parts.slice(0, -2).join(":");
47
+ if (exp !== 0 && Math.floor(Date.now() / 1000) > exp)
48
+ return null;
49
+ const signed = `${payload}:${exp}`;
50
+ return verifySignature(signed, sig, secrets) ? payload : null;
51
+ }
52
+ function verifyUpgradeToken(token, secrets) {
53
+ return verifyToken(token, secrets);
54
+ }
55
+
56
+ // connector/descriptor.ts
57
+ var CONTRACT_VERSION = 1;
58
+ var AGENTSCHAT_MAX_MESSAGE_LENGTH = 4000;
59
+ var SUPPORTED_OPS = ["send", "typing", "get_chat_info"];
60
+ function buildDescriptor(overrides = {}) {
61
+ return {
62
+ contract_version: CONTRACT_VERSION,
63
+ platform: "agentschat",
64
+ label: "AgentsChat",
65
+ max_message_length: AGENTSCHAT_MAX_MESSAGE_LENGTH,
66
+ supports_draft_streaming: false,
67
+ supports_edit: false,
68
+ supports_threads: false,
69
+ markdown_dialect: "markdown",
70
+ len_unit: "chars",
71
+ emoji: "\uD83E\uDD16",
72
+ pii_safe: false,
73
+ supported_ops: [...SUPPORTED_OPS],
74
+ ...overrides
75
+ };
76
+ }
77
+
78
+ // connector/normalize.ts
79
+ function toWireEvent(msg, platform = "agentschat") {
80
+ const content = msg.content ?? "";
81
+ if (content === "__typing__")
82
+ return null;
83
+ const chatId = msg.channel_id ?? "";
84
+ if (!chatId)
85
+ return null;
86
+ const isDm = chatId.startsWith("dm-");
87
+ return {
88
+ text: content,
89
+ message_type: "text",
90
+ message_id: msg.id,
91
+ reply_to_message_id: msg.reply_to,
92
+ source: {
93
+ platform,
94
+ chat_id: chatId,
95
+ chat_type: isDm ? "dm" : "group",
96
+ chat_name: msg.channel_id ?? null,
97
+ user_id: msg.sender_id,
98
+ user_name: msg.sender_name ?? msg.sender_id,
99
+ thread_id: null
100
+ }
101
+ };
102
+ }
103
+
104
+ // connector/server.ts
105
+ function startConnector(config) {
106
+ const log = config.logger ?? (() => {});
107
+ const descriptor = buildDescriptor(config.descriptor);
108
+ const sockets = new Set;
109
+ const http = createServer((_req, res) => {
110
+ res.writeHead(200, { "Content-Type": "application/json" });
111
+ res.end(JSON.stringify({ ok: true, service: "agentschat-connector", contract_version: 1 }));
112
+ });
113
+ const wss = new WebSocketServer({ noServer: true });
114
+ http.on("upgrade", (req, socket, head) => {
115
+ const { pathname } = new URL(req.url ?? "/", "http://localhost");
116
+ if (pathname !== "/relay") {
117
+ socket.destroy();
118
+ return;
119
+ }
120
+ const auth = req.headers.authorization ?? "";
121
+ const token = auth.startsWith("Bearer ") ? auth.slice(7).trim() : "";
122
+ const payload = peekPayload(token);
123
+ const secrets = payload ? config.secrets[payload] : undefined;
124
+ const gatewayId = secrets ? verifyUpgradeToken(token, secrets) : null;
125
+ if (!gatewayId) {
126
+ log(`[connector] rejecting upgrade: bad/absent token (path=${pathname})`);
127
+ wss.handleUpgrade(req, socket, head, (ws) => {
128
+ ws.close(CLOSE_UNAUTHORIZED, "unauthorized");
129
+ });
130
+ return;
131
+ }
132
+ wss.handleUpgrade(req, socket, head, (ws) => {
133
+ onConnection(ws, gatewayId);
134
+ });
135
+ });
136
+ function onConnection(ws, gatewayId) {
137
+ sockets.add(ws);
138
+ log(`[connector] gateway connected: ${gatewayId} (${sockets.size} total)`);
139
+ ws.on("message", async (data) => {
140
+ let frame;
141
+ try {
142
+ frame = JSON.parse(data.toString());
143
+ } catch {
144
+ return;
145
+ }
146
+ await handleFrame(ws, frame).catch((e) => log(`[connector] frame error: ${e}`));
147
+ });
148
+ ws.on("close", () => {
149
+ sockets.delete(ws);
150
+ log(`[connector] gateway disconnected: ${gatewayId} (${sockets.size} left)`);
151
+ });
152
+ ws.on("error", () => sockets.delete(ws));
153
+ }
154
+ async function handleFrame(ws, frame) {
155
+ const t = frame?.type;
156
+ if (t === "hello") {
157
+ send(ws, { type: "descriptor", descriptor });
158
+ return;
159
+ }
160
+ if (t === "outbound") {
161
+ const result = await handleOutbound(frame.action ?? {});
162
+ send(ws, { type: "outbound_result", requestId: frame.requestId, result });
163
+ return;
164
+ }
165
+ }
166
+ async function handleOutbound(action) {
167
+ const op = action?.op;
168
+ const chatId = action?.chat_id ?? "";
169
+ switch (op) {
170
+ case "send": {
171
+ const r = await config.agentschat.sendMessage(chatId, action.content ?? "", action.reply_to);
172
+ return { success: true, message_id: r?.id };
173
+ }
174
+ case "typing": {
175
+ await config.agentschat.sendTyping?.(chatId);
176
+ return { success: true };
177
+ }
178
+ case "get_chat_info": {
179
+ const info = await config.agentschat.getChatInfo(chatId);
180
+ return info ?? {};
181
+ }
182
+ default:
183
+ return { success: false, error: `unsupported op: ${String(op)}` };
184
+ }
185
+ }
186
+ function send(ws, obj) {
187
+ if (ws.readyState === WebSocket.OPEN)
188
+ ws.send(JSON.stringify(obj) + `
189
+ `);
190
+ }
191
+ http.listen(config.port, config.host ?? "127.0.0.1");
192
+ const address = http.address();
193
+ const port = typeof address === "object" && address ? address.port : config.port;
194
+ return {
195
+ port,
196
+ stop() {
197
+ for (const ws of sockets) {
198
+ try {
199
+ ws.close(1001, "connector shutdown");
200
+ } catch {}
201
+ }
202
+ wss.close();
203
+ http.close();
204
+ },
205
+ injectAgentsChatMessage(msg) {
206
+ const event = toWireEvent(msg, descriptor.platform);
207
+ if (!event)
208
+ return;
209
+ for (const ws of sockets)
210
+ send(ws, { type: "inbound", event });
211
+ },
212
+ connections() {
213
+ return sockets.size;
214
+ }
215
+ };
216
+ }
217
+ function peekPayload(token) {
218
+ try {
219
+ const decoded = Buffer.from(token, "base64url").toString("utf8");
220
+ const parts = decoded.split(":");
221
+ if (parts.length < 3)
222
+ return null;
223
+ return parts.slice(0, -2).join(":");
224
+ } catch {
225
+ return null;
226
+ }
227
+ }
228
+
229
+ // connector/run.ts
230
+ import WS from "ws";
231
+ var log = (m) => process.stderr.write(`[agentschat-connector] ${m}
232
+ `);
233
+ function need(name) {
234
+ const v = process.env[name];
235
+ if (!v) {
236
+ log(`ERROR: ${name} is required`);
237
+ process.exit(1);
238
+ }
239
+ return v;
240
+ }
241
+ var AGENT_ID = need("AGENTCHAT_AGENT_ID");
242
+ var TOKEN = need("AGENTCHAT_TOKEN");
243
+ var GATEWAY_ID = need("RELAY_GATEWAY_ID");
244
+ var GATEWAY_SECRET = need("RELAY_GATEWAY_SECRET");
245
+ var API = (process.env.AGENTCHAT_API_URL || "https://agents-chat.com").replace(/\/$/, "");
246
+ var WS_URL = process.env.AGENTCHAT_WS_URL || API.replace(/^http/, "ws") + "/ws";
247
+ var PORT = Number(process.env.RELAY_PORT || 8765);
248
+ var HOST = process.env.RELAY_HOST || "127.0.0.1";
249
+ var broadcast = null;
250
+ var connector = startConnector({
251
+ port: PORT,
252
+ host: HOST,
253
+ secrets: { [GATEWAY_ID]: [GATEWAY_SECRET] },
254
+ agentschat: {
255
+ async sendMessage(chatId, content, replyTo) {
256
+ const res = await fetch(`${API}/api/channels/${encodeURIComponent(chatId)}/messages`, {
257
+ method: "POST",
258
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${TOKEN}` },
259
+ body: JSON.stringify({ sender_id: AGENT_ID, content_type: "text", content, ...replyTo ? { parent_id: replyTo } : {} })
260
+ });
261
+ if (!res.ok)
262
+ throw new Error(`agentschat send failed: ${res.status}`);
263
+ const data = await res.json();
264
+ return { id: data?.id };
265
+ },
266
+ async getChatInfo(chatId) {
267
+ const res = await fetch(`${API}/api/channels/${encodeURIComponent(chatId)}`, {
268
+ headers: { Authorization: `Bearer ${TOKEN}` }
269
+ });
270
+ if (!res.ok)
271
+ return { name: chatId, type: chatId.startsWith("dm-") ? "dm" : "group" };
272
+ const data = await res.json();
273
+ return { name: data?.name ?? chatId, type: chatId.startsWith("dm-") ? "dm" : "group" };
274
+ },
275
+ async sendTyping(chatId) {
276
+ sendAgentsChatFrame({ type: "typing", channel_id: chatId, sender_id: AGENT_ID });
277
+ }
278
+ },
279
+ logger: log
280
+ });
281
+ broadcast = (msg) => connector.injectAgentsChatMessage(msg);
282
+ log(`listening on ${HOST}:${connector.port} (contract v1, gateway id "${GATEWAY_ID}")`);
283
+ var ws = null;
284
+ var reconnectDelay = 1000;
285
+ function sendAgentsChatFrame(frame) {
286
+ if (ws && ws.readyState === WS.OPEN) {
287
+ try {
288
+ ws.send(JSON.stringify(frame));
289
+ } catch {}
290
+ }
291
+ }
292
+ function connectAgentsChat() {
293
+ ws = new WS(WS_URL);
294
+ ws.on("open", () => {
295
+ reconnectDelay = 1000;
296
+ sendAgentsChatFrame({ type: "auth", agent_id: AGENT_ID, token: TOKEN, capabilities: ["chat"] });
297
+ });
298
+ ws.on("message", (raw) => {
299
+ let data;
300
+ try {
301
+ data = JSON.parse(String(raw));
302
+ } catch {
303
+ return;
304
+ }
305
+ if (data.type === "auth_ok") {
306
+ log(`connected to agentschat as ${AGENT_ID}`);
307
+ return;
308
+ }
309
+ if (data.type === "message" && data.sender_id !== AGENT_ID) {
310
+ broadcast?.(data);
311
+ }
312
+ });
313
+ ws.on("close", () => {
314
+ if (process.__shutdown)
315
+ return;
316
+ log(`agentschat WS closed; reconnecting in ${reconnectDelay}ms`);
317
+ setTimeout(connectAgentsChat, reconnectDelay);
318
+ reconnectDelay = Math.min(reconnectDelay * 2, 30000);
319
+ });
320
+ ws.on("error", (e) => {
321
+ log(`agentschat WS error: ${e?.message ?? e}`);
322
+ });
323
+ }
324
+ connectAgentsChat();
325
+ for (const sig of ["SIGINT", "SIGTERM"]) {
326
+ process.on(sig, () => {
327
+ process.__shutdown = true;
328
+ try {
329
+ ws?.close();
330
+ } catch {}
331
+ connector.stop();
332
+ process.exit(0);
333
+ });
334
+ }