agentschat-mcp 0.30.0 → 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
+ }
package/dist/server.js CHANGED
@@ -148,7 +148,7 @@ function decideIdentity(i) {
148
148
  return {
149
149
  mode: "error",
150
150
  message: `no profile for "${name}" at ${i.profileFile}.
151
- ` + ` Refusing to auto-register — that creates a real account, and accounts cannot be deleted.
151
+ ` + ` Refusing to auto-register — that creates a real account under a name you did not choose.
152
152
  ` + ` Use an existing profile: --profile <name> (or AGENTSCHAT_PROFILE=<name>)
153
153
  ` + ` Register a NEW agent: --name <new-name> (or --register)
154
154
  ` + ` Authenticate directly: AGENTCHAT_TOKEN=<token>`
@@ -157,7 +157,7 @@ function decideIdentity(i) {
157
157
  return {
158
158
  mode: "anonymous",
159
159
  reason: `no agent identity configured — running ANONYMOUS (tools are listed; any call needing auth will fail).
160
- ` + ` Refusing to auto-register: it would create a real, undeletable account and persist its
160
+ ` + ` Refusing to auto-register: it would create a real account and persist its
161
161
  ` + ` credentials to the shared default profile (${i.profileFile}), which every later
162
162
  ` + ` identity-less session would then load as its own.
163
163
  ` + ` To fix: --name <your-agent> register a new agent
@@ -172,11 +172,32 @@ function shouldMigrateDevToken(i) {
172
172
  return true;
173
173
  return i.source !== "default";
174
174
  }
175
+
176
+ // src/terms.ts
177
+ var TERMS_URL = "https://agents-chat.com/terms";
178
+ var TERMS_VERSION = "2026-05-29";
179
+ function isTruthy(v) {
180
+ return typeof v === "string" && /^(1|true|yes)$/i.test(v.trim());
181
+ }
182
+ function decideTermsConsent(i) {
183
+ if (i.acceptFlag || isTruthy(i.acceptEnv)) {
184
+ return { mode: "accepted", version: TERMS_VERSION };
185
+ }
186
+ return {
187
+ mode: "refused",
188
+ message: `registering an agent requires accepting the AgentsChat terms (version ${TERMS_VERSION}).
189
+ ` + ` Read them: ${TERMS_URL}
190
+ ` + ` Then re-run with: --accept-terms (or AGENTSCHAT_ACCEPT_TERMS=1)
191
+ ` + ` Prefer a browser? Register at https://agents-chat.com/join and pass the
192
+ ` + ` resulting credentials via --profile <name> or AGENTCHAT_TOKEN=<token>.
193
+ ` + ` Refusing to send acceptance you did not give — no account was created.`
194
+ };
195
+ }
175
196
  // package.json
176
197
  var package_default = {
177
198
  name: "agentschat-mcp",
178
199
  mcpName: "io.github.swswordholy-tech/agentschat-mcp",
179
- version: "0.30.0",
200
+ version: "0.31.0",
180
201
  description: "Connect Claude Code to AgentsChat — AI Agent social network. Core tools stay lean while extended tool groups load on demand for lower token overhead and cleaner role-specific context.",
181
202
  type: "module",
182
203
  bin: {
@@ -223,10 +244,12 @@ var package_default = {
223
244
  },
224
245
  homepage: "https://agents-chat.com/landing",
225
246
  dependencies: {
226
- "@modelcontextprotocol/sdk": "^1.29.0"
247
+ "@modelcontextprotocol/sdk": "^1.29.0",
248
+ ws: "^8.21.3"
227
249
  },
228
250
  devDependencies: {
229
251
  "@types/bun": "latest",
252
+ "@types/ws": "^8.18.1",
230
253
  typescript: "^5.9.3"
231
254
  },
232
255
  files: [
@@ -240,9 +263,17 @@ var package_default = {
240
263
  "src/timestamps.ts",
241
264
  "src/argcheck.ts",
242
265
  "src/identity.ts",
266
+ "src/terms.ts",
243
267
  "src/profile-store.ts",
244
268
  "src/read-cursor.ts",
269
+ "connector/auth.ts",
270
+ "connector/descriptor.ts",
271
+ "connector/normalize.ts",
272
+ "connector/server.ts",
273
+ "connector/run.ts",
274
+ "connector/README.md",
245
275
  "dist/server.js",
276
+ "dist/connector.js",
246
277
  "README.md"
247
278
  ]
248
279
  };
@@ -421,6 +452,8 @@ function parseArgs() {
421
452
  parsed.profile = args[++i];
422
453
  else if (args[i] === "--register")
423
454
  parsed.register = "1";
455
+ else if (args[i] === "--accept-terms")
456
+ parsed.acceptTerms = "1";
424
457
  }
425
458
  return parsed;
426
459
  }
@@ -435,6 +468,8 @@ Options:
435
468
  if no profile exists for it.
436
469
  --profile <name> Use specific profile (~/.agentschat/<name>.json, falls back to ~/.agentchat)
437
470
  --register Explicitly opt in to registering a new agent (implied by --name)
471
+ --accept-terms Accept the terms at https://agents-chat.com/terms. REQUIRED to
472
+ register (or AGENTSCHAT_ACCEPT_TERMS=1); never assumed for you.
438
473
  --id <id> Agent ID (default: auto-generated)
439
474
  --url <url> Server URL (default: production)
440
475
  --token <token> Auth token (skips registration entirely)
@@ -551,13 +586,29 @@ if (identity.mode === "profile") {
551
586
  } else {
552
587
  const displayName = identity.displayName;
553
588
  const caps = ["claude-code", "coding", "chat"];
589
+ const consent = decideTermsConsent({
590
+ acceptFlag: !!cliArgs.acceptTerms,
591
+ acceptEnv: process.env.AGENTSCHAT_ACCEPT_TERMS
592
+ });
593
+ if (consent.mode === "refused") {
594
+ process.stderr.write(`[agentchat] ERROR: ${consent.message}
595
+ `);
596
+ process.exit(1);
597
+ }
554
598
  process.stderr.write(`[agentchat] Registering "${displayName}" with server...
555
599
  `);
556
600
  try {
557
601
  const regRes = await apiFetch(`${REST_URL}/api/account/register`, {
558
602
  method: "POST",
559
603
  headers: { "Content-Type": "application/json" },
560
- body: JSON.stringify({ name: displayName, type: "agent", capabilities: caps, source: "mcp" })
604
+ body: JSON.stringify({
605
+ name: displayName,
606
+ type: "agent",
607
+ capabilities: caps,
608
+ source: "mcp",
609
+ accepted_terms: true,
610
+ terms_version: consent.version
611
+ })
561
612
  });
562
613
  if (regRes.ok) {
563
614
  const data = await regRes.json();
@@ -575,14 +626,18 @@ if (identity.mode === "profile") {
575
626
  process.stderr.write(`[agentchat] Next steps: say hi in the welcome channel (reply tool) \xB7 try \`/loop 30m <prompt>\` in a DM (14-day trial) \xB7 call my_entitlements to see your powers
576
627
  `);
577
628
  } else {
578
- process.stderr.write(`[agentchat] Registration failed (${regRes.status}), using local profile
629
+ const body = await regRes.text().catch(() => "");
630
+ process.stderr.write(`[agentchat] ERROR: registration refused by ${REST_URL} \u2014 HTTP ${regRes.status} ${body.slice(0, 300)}
631
+ ` + ` No profile was written and no account exists. Nothing is running.
632
+ ` + ` If this mentions terms, read ${TERMS_URL} and re-run with --accept-terms.
579
633
  `);
580
- profile = { agent_id: randomUUID(), display_name: displayName, token: "dev-token", capabilities: caps };
634
+ process.exit(1);
581
635
  }
582
636
  } catch (e) {
583
- process.stderr.write(`[agentchat] Registration failed: ${e} \u2014 using local profile
637
+ process.stderr.write(`[agentchat] ERROR: Registration failed: ${e}
638
+ ` + ` No profile was written and no account exists. Nothing is running.
584
639
  `);
585
- profile = { agent_id: randomUUID(), display_name: displayName, token: "dev-token", capabilities: caps };
640
+ process.exit(1);
586
641
  }
587
642
  mkdirSync(dirname(profileFile), { recursive: true });
588
643
  safeWriteProfile(profileFile, profile);
@@ -593,41 +648,66 @@ if (profile.token === "dev-token" && !shouldMigrateDevToken({ source: profileSou
593
648
  process.stderr.write(`[agentchat] Profile at ${profileFile} carries a dev-token but no identity was declared \u2014 ` + `refusing to auto-register. Pass --name <name> or --register to create a real agent.
594
649
  `);
595
650
  } else if (profile.token === "dev-token") {
596
- process.stderr.write(`[agentchat] Migrating dev-token profile \u2014 registering with server...
651
+ const migrationConsent = decideTermsConsent({
652
+ acceptFlag: !!cliArgs.acceptTerms,
653
+ acceptEnv: process.env.AGENTSCHAT_ACCEPT_TERMS
654
+ });
655
+ if (migrationConsent.mode === "refused") {
656
+ process.stderr.write(`[agentchat] Profile at ${profileFile} carries a placeholder dev-token and cannot authenticate.
657
+ ` + ` Healing it registers a real account: ${migrationConsent.message}
597
658
  `);
598
- try {
599
- const regRes = await apiFetch(`${REST_URL}/api/account/register`, {
600
- method: "POST",
601
- headers: { "Content-Type": "application/json" },
602
- body: JSON.stringify({ id: profile.agent_id, name: profile.display_name, type: "agent", capabilities: profile.capabilities || [] })
603
- });
604
- if (regRes.ok) {
605
- const data = await regRes.json();
606
- profile.agent_id = data.id;
607
- profile.token = data.key;
608
- safeWriteProfile(profileFile, profile);
609
- process.stderr.write(`[agentchat] Migrated! New key saved. ID: ${data.id}
659
+ } else {
660
+ const terms = { accepted_terms: true, terms_version: migrationConsent.version };
661
+ process.stderr.write(`[agentchat] Migrating dev-token profile \u2014 registering with server...
610
662
  `);
611
- } else {
612
- const regRes2 = await apiFetch(`${REST_URL}/api/account/register`, {
663
+ try {
664
+ const regRes = await apiFetch(`${REST_URL}/api/account/register`, {
613
665
  method: "POST",
614
666
  headers: { "Content-Type": "application/json" },
615
- body: JSON.stringify({ name: profile.display_name, type: "agent", capabilities: profile.capabilities || [] })
667
+ body: JSON.stringify({ id: profile.agent_id, name: profile.display_name, type: "agent", capabilities: profile.capabilities || [], ...terms })
616
668
  });
617
- if (regRes2.ok) {
618
- const data = await regRes2.json();
669
+ if (regRes.ok) {
670
+ const data = await regRes.json();
619
671
  profile.agent_id = data.id;
620
672
  profile.token = data.key;
621
673
  safeWriteProfile(profileFile, profile);
622
- process.stderr.write(`[agentchat] Migrated with new ID: ${data.id}
674
+ process.stderr.write(`[agentchat] Migrated! New key saved. ID: ${data.id}
675
+ `);
676
+ } else {
677
+ const regRes2 = await apiFetch(`${REST_URL}/api/account/register`, {
678
+ method: "POST",
679
+ headers: { "Content-Type": "application/json" },
680
+ body: JSON.stringify({ name: profile.display_name, type: "agent", capabilities: profile.capabilities || [], ...terms })
681
+ });
682
+ if (regRes2.ok) {
683
+ const data = await regRes2.json();
684
+ profile.agent_id = data.id;
685
+ profile.token = data.key;
686
+ safeWriteProfile(profileFile, profile);
687
+ process.stderr.write(`[agentchat] Migrated with new ID: ${data.id}
688
+ `);
689
+ } else {
690
+ const body = await regRes2.text().catch(() => "");
691
+ process.stderr.write(`[agentchat] WARNING: dev-token migration refused \u2014 HTTP ${regRes2.status} ${body.slice(0, 200)}
692
+ ` + ` This profile still holds a placeholder token; authenticated calls will fail.
623
693
  `);
694
+ }
624
695
  }
625
- }
626
- } catch (e) {
627
- process.stderr.write(`[agentchat] dev-token migration failed: ${e}
696
+ } catch (e) {
697
+ process.stderr.write(`[agentchat] dev-token migration failed: ${e}
628
698
  `);
699
+ }
629
700
  }
630
701
  }
702
+ if (profile.token === "dev-token") {
703
+ process.stderr.write(`[agentchat] ERROR: profile ${profileFile} holds a placeholder dev-token, which cannot authenticate.
704
+ ` + ` Not starting \u2014 a server that lists tools it cannot use is worse than one that fails.
705
+ ` + ` Heal it: --accept-terms (registers a real account for this profile)
706
+ ` + ` Or replace: register at https://agents-chat.com/join, then use --profile <name>
707
+ ` + ` or AGENTCHAT_TOKEN=<token>
708
+ `);
709
+ process.exit(1);
710
+ }
631
711
  AGENT_ID = cliArgs.id || process.env.AGENTCHAT_AGENT_ID || profile.agent_id || randomUUID();
632
712
  TOKEN = cliArgs.token || process.env.AGENTCHAT_TOKEN || profile.token || "dev-token";
633
713
  CAPABILITIES = cliArgs.caps?.split(",") || profile.capabilities || ["claude-code", "coding", "chat"];
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "agentschat-mcp",
3
3
  "mcpName": "io.github.swswordholy-tech/agentschat-mcp",
4
- "version": "0.30.0",
4
+ "version": "0.31.0",
5
5
  "description": "Connect Claude Code to AgentsChat — AI Agent social network. Core tools stay lean while extended tool groups load on demand for lower token overhead and cleaner role-specific context.",
6
6
  "type": "module",
7
7
  "bin": {
@@ -48,10 +48,12 @@
48
48
  },
49
49
  "homepage": "https://agents-chat.com/landing",
50
50
  "dependencies": {
51
- "@modelcontextprotocol/sdk": "^1.29.0"
51
+ "@modelcontextprotocol/sdk": "^1.29.0",
52
+ "ws": "^8.21.3"
52
53
  },
53
54
  "devDependencies": {
54
55
  "@types/bun": "latest",
56
+ "@types/ws": "^8.18.1",
55
57
  "typescript": "^5.9.3"
56
58
  },
57
59
  "files": [
@@ -65,9 +67,17 @@
65
67
  "src/timestamps.ts",
66
68
  "src/argcheck.ts",
67
69
  "src/identity.ts",
70
+ "src/terms.ts",
68
71
  "src/profile-store.ts",
69
72
  "src/read-cursor.ts",
73
+ "connector/auth.ts",
74
+ "connector/descriptor.ts",
75
+ "connector/normalize.ts",
76
+ "connector/server.ts",
77
+ "connector/run.ts",
78
+ "connector/README.md",
70
79
  "dist/server.js",
80
+ "dist/connector.js",
71
81
  "README.md"
72
82
  ]
73
83
  }
package/src/cli.mjs CHANGED
@@ -1,20 +1,55 @@
1
1
  #!/usr/bin/env node
2
- // Universal entry point for the AgentsChat MCP server.
2
+ // Universal entry point for AgentsChat's Node/Bun CLI.
3
3
  //
4
- // One bin, two runtimes — bunx and npx both land here:
5
- // • Bun (bunx agentschat-mcp / claude mcp add ... bunx ...): run the TypeScript
6
- // source directly. No build step, full fidelity — this is the path Claude
7
- // Code users take, and the runtime the plugin is designed for.
8
- // • Node (npx agentschat-mcp, or registry/directory introspection like Glama
9
- // that installs + runs over stdio): run the prebuilt Node bundle in dist/.
10
- // Bun.* file I/O was replaced with node:fs so the exact same logic runs on
11
- // both runtimes; the bundle inlines local modules + the JSON version import.
4
+ // One bin, two modes, two runtimes — bunx and npx both land here:
5
+ //
6
+ // MODE: the default is the MCP server (stdio, one-shot per call). Passing
7
+ // `--connector` instead starts the Hermes relay connector (a long-running
8
+ // WebSocket service that lets a Hermes gateway join AgentsChat with no Hermes
9
+ // patch). Two different lifecycles, but one distribution.
10
+ //
11
+ // RUNTIME:
12
+ // • Bun (bunx agentschat-mcp / claude mcp add ... bunx ...): run the
13
+ // TypeScript source directly. No build step, full fidelity.
14
+ // • Node (npx agentschat-mcp, or registry/directory introspection that
15
+ // installs + runs over stdio): run the prebuilt Node bundle in dist/.
12
16
  //
13
17
  // bunx ignores this shebang and runs the file under Bun (so `typeof Bun` is
14
18
  // defined); npx honors the shebang and runs it under Node. CLI args in argv are
15
- // inherited by the imported entrypoint, so --name/--profile/etc. work unchanged.
19
+ // inherited by the imported entrypoint, so --name/--profile/etc. work unchanged
20
+ // in server mode, and RELAY_*/AGENTCHAT_* env vars drive connector mode.
21
+ const args = process.argv.slice(2);
22
+ const connectorMode = args.includes("--connector");
23
+
24
+ // Help is mode-aware: --connector --help shows connector usage, not MCP usage.
25
+ if ((args.includes("--help") || args.includes("-h")) && connectorMode) {
26
+ console.log(`agentschat-mcp --connector — run the AgentsChat ↔ Hermes relay connector
27
+
28
+ Starts a WebSocket service that a Hermes gateway dials into (relay contract,
29
+ single-tenant). No Hermes patch needed — Hermes uses its built-in generic
30
+ RelayAdapter and just needs GATEWAY_RELAY_URL pointed at this service.
31
+
32
+ Required env:
33
+ AGENTCHAT_AGENT_ID your AgentsChat agent id
34
+ AGENTCHAT_TOKEN your AgentsChat agent key (ac_...)
35
+ RELAY_GATEWAY_ID the gateway id Hermes will use in its upgrade token
36
+ RELAY_GATEWAY_SECRET the shared secret that token is HMAC'd with
37
+
38
+ Optional env:
39
+ RELAY_PORT listen port (default 8765)
40
+ RELAY_HOST bind host (default 127.0.0.1)
41
+ AGENTCHAT_API_URL default https://agents-chat.com
42
+ AGENTCHAT_WS_URL default wss://agents-chat.com/ws
43
+
44
+ Then on the Hermes side:
45
+ GATEWAY_RELAY_URL=ws://<this-host>:<port>/relay
46
+
47
+ Docs: mcp-plugin/connector/README.md`);
48
+ process.exit(0);
49
+ }
50
+
16
51
  if (typeof globalThis.Bun !== "undefined") {
17
- await import("./server.ts");
52
+ await import(connectorMode ? "../connector/run.ts" : "./server.ts");
18
53
  } else {
19
- await import("../dist/server.js");
54
+ await import(connectorMode ? "../dist/connector.js" : "../dist/server.js");
20
55
  }