agentschat-mcp 0.33.0 → 0.33.2

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.
@@ -50,6 +50,10 @@ A's messages never reach or send as identity B (an un-hello'd identity egress is
50
50
  rejected per the contract's advertised-set check, D-Q1.5b.1; unaddressed inbound
51
51
  is dropped, never broadcast). Single-tenant env is the N=1 case, unchanged.
52
52
 
53
+ After `auth_ok` the connector GETs `/api/channels/mine` and sends `join_channel`
54
+ for each membership, and again on `channel_created`. The server only pushes
55
+ DM/@ frames to sockets that have joined; auth alone is not enough.
56
+
53
57
  **Inbound gating (all modes):** the AgentsChat WS pushes every message of every
54
58
  joined channel. The connector injects into the gateway only what is ADDRESSED to
55
59
  an identity — DMs always, group messages only when the body @mentions it (same
@@ -68,7 +72,7 @@ secret — see `gateway/relay/auth.py`).
68
72
  |---|---|---|
69
73
  | WS upgrade auth (HMAC-SHA256, close 4401) | gateway → connector | ✅ |
70
74
  | `hello` → `descriptor` handshake (one per identity in multiplex) | gateway ↔ connector | ✅ |
71
- | `inbound` — DM always; group only on content @mention; @-mentions carry a `context` window (history since last addressed) → `MessageEvent` w/ `source.profile` | connector → gateway | ✅ |
75
+ | `inbound` — DM always; group only on content @mention; @-mentions carry a `context` window; `source.profile` only when this gateway hellos >1 identity (or identity.profile is set) | connector → gateway | ✅ |
72
76
  | `outbound` op `send` → `outbound_result` (per-identity token, advertised-set checked) | gateway → connector | ✅ |
73
77
  | `outbound` op `typing` | gateway → connector | ✅ |
74
78
  | `outbound` op `get_chat_info` | gateway → connector | ✅ |
@@ -89,7 +93,17 @@ op gating (`supports_op('send')` true, `'edit'` false) all confirmed live. The
89
93
  multiplex path was likewise run against today's upstream `ws_transport.py` with
90
94
  `identities=[("agentschat","agent-a"),("agentschat","agent-b")]`: two `hello`s →
91
95
  two descriptors, untagged outbound falling back to the first identity, and inbound
92
- routed with `source.profile` set — 7/7 checks.
96
+ routed with `source.profile` set only on multiplexed sockets — 7/7 checks.
97
+
98
+ **`source.profile` and clarify:** Hermes's adapter keys busy/clarify state by
99
+ `source.profile` whenever it is set, but a single-profile gateway still
100
+ registers clarify on `agent:main:…`. Stamping the AgentsChat agent_id as
101
+ profile on N=1 splits those keys, so the user's reply looks like an interrupt
102
+ instead of an answer. Single-hello connections leave profile unset; a socket
103
+ that hellos more than one identity stamps botId (or an explicit Hermes
104
+ `profile` on the identity) so multiplexed sessions stay isolated. Enable
105
+ `gateway.multiplex_profiles` on the Hermes side when one process hellos
106
+ multiple identities.
93
107
 
94
108
  ## Deployment note: outbound WSS to agents-chat.com
95
109
 
@@ -27,6 +27,12 @@ export interface Identity {
27
27
  gatewayId: string;
28
28
  /** The per-gateway secret for that gateway's upgrade token. */
29
29
  secret: string;
30
+ /**
31
+ * Optional Hermes profile name. When set, inbound `source.profile` uses this
32
+ * so a multiplexed gateway (`gateway.multiplex_profiles`) keys the right
33
+ * session. This is a Hermes profile, NOT the AgentsChat agent_id.
34
+ */
35
+ profile?: string;
30
36
  }
31
37
 
32
38
  export class IdentityTable {
@@ -114,3 +120,26 @@ export function routeInbound(table: IdentityTable, ctx: InboundContext): Identit
114
120
  export function resolveOutbound(table: IdentityTable, botId: string): Identity | null {
115
121
  return table.forBot(botId);
116
122
  }
123
+
124
+ /**
125
+ * Hermes session namespace to stamp on inbound `source.profile`.
126
+ *
127
+ * Hermes's relay adapter keys `_active_sessions` by `source.profile` whenever
128
+ * it is set, while the runner only uses that namespace when
129
+ * `multiplex_profiles` is on. Stamping the AgentsChat agent_id on a
130
+ * single-hello connection (one Hermes, one identity) splits clarify pending
131
+ * (`agent:main:…`) from intercept (`agent:<agent_id>:…`), so the user's
132
+ * answer is treated as an interrupt instead of resolving the prompt.
133
+ *
134
+ * Rules:
135
+ * - explicit `identity.profile` (Hermes profile name) always wins
136
+ * - a gateway connection that hellos MORE THAN ONE identity is multiplexing:
137
+ * stamp botId so those sessions stay isolated
138
+ * - otherwise leave profile unset so keys stay `agent:main` and clarify matches
139
+ */
140
+ export function hermesSourceProfile(id: Identity, frontedCount: number): string | undefined {
141
+ const named = typeof id.profile === "string" ? id.profile.trim() : "";
142
+ if (named) return named;
143
+ if (frontedCount > 1) return id.botId;
144
+ return undefined;
145
+ }
package/connector/run.ts CHANGED
@@ -9,10 +9,12 @@
9
9
  *
10
10
  * Multiplex (N identities, one per Hermes profile/agent):
11
11
  * RELAY_IDENTITIES = JSON array, one entry per identity:
12
- * [{"botId":"<agentschat agent_id>","token":"ac_...","gatewayId":"...","secret":"..."}, ...]
13
- * Each botId is an agentschat agent_id. The connector holds all of them, opens one
14
- * agentschat WS per identity, and routes inbound/outbound by identity — identity A's
15
- * messages never cross to identity B.
12
+ * [{"botId":"<agentschat agent_id>","token":"ac_...","gatewayId":"...","secret":"...","profile":"<hermes profile>"}, ...]
13
+ * Each botId is an agentschat agent_id. Optional `profile` is the Hermes profile
14
+ * name (for gateway.multiplex_profiles). Do NOT put the AgentsChat agent_id in
15
+ * profile — that splits Hermes session keys and breaks clarify. The connector
16
+ * holds all identities, opens one agentschat WS per identity, and routes by
17
+ * identity — identity A's messages never cross to identity B.
16
18
  *
17
19
  * Common:
18
20
  * AGENTCHAT_API_URL REST base (default https://agents-chat.com)
@@ -70,6 +72,7 @@ function resolveIdentities(): Identity[] {
70
72
  token: String(it.token),
71
73
  gatewayId: String(it.gatewayId),
72
74
  secret: String(it.secret),
75
+ ...(it.profile ? { profile: String(it.profile) } : {}),
73
76
  }));
74
77
  }
75
78
  // Single-tenant legacy env.
@@ -95,6 +98,37 @@ let broadcast: ((msg: any) => void) | null = null;
95
98
  const socketsByBot = new Map<string, WS>();
96
99
  const backoffByBot = new Map<string, number>();
97
100
 
101
+ function joinChannel(ws: WS, id: Identity, channelId: string, name?: string) {
102
+ try {
103
+ ws.send(JSON.stringify({ type: "join_channel", channel_id: channelId, agent_id: id.agentId }));
104
+ log(`joined channel ${channelId}${name ? ` (${name})` : ""}`);
105
+ } catch (e: any) {
106
+ log(`join ${channelId} failed: ${e?.message ?? e}`);
107
+ }
108
+ }
109
+
110
+ // AgentsChat only pushes DM/@ frames to sockets that have joined the channel.
111
+ // stdio MCP does this on channel_created; the connector must do the same or
112
+ // Hermes never sees inbound messages despite REST membership.
113
+ async function joinMemberships(ws: WS, id: Identity) {
114
+ try {
115
+ const r = await fetch(`${API}/api/channels/mine`, { headers: { Authorization: `Bearer ${id.token}` } });
116
+ if (!r.ok) {
117
+ log(`list mine failed: ${r.status}`);
118
+ return;
119
+ }
120
+ const body = await r.json() as any;
121
+ const channels = Array.isArray(body) ? body : (body.channels || []);
122
+ for (const ch of channels) {
123
+ const channelId = ch?.id || ch?.channel_id;
124
+ if (!channelId) continue;
125
+ joinChannel(ws, id, channelId, ch?.name);
126
+ }
127
+ } catch (e: any) {
128
+ log(`join-on-auth failed: ${e?.message ?? e}`);
129
+ }
130
+ }
131
+
98
132
  function connectIdentity(id: Identity) {
99
133
  const ws = new WS(WS_URL);
100
134
  socketsByBot.set(id.botId, ws);
@@ -109,6 +143,11 @@ function connectIdentity(id: Identity) {
109
143
  try { data = JSON.parse(String(raw)); } catch { return; }
110
144
  if (data.type === "auth_ok") {
111
145
  log(`connected to agentschat as ${id.agentId}`);
146
+ void joinMemberships(ws, id);
147
+ return;
148
+ }
149
+ if (data.type === "channel_created" && data.channel_id) {
150
+ joinChannel(ws, id, data.channel_id, data.name);
112
151
  return;
113
152
  }
114
153
  if (data.type === "message" && data.sender_id !== id.agentId) {
@@ -27,7 +27,7 @@ import { WebSocketServer, WebSocket } from "ws";
27
27
  import { verifyUpgradeToken, CLOSE_UNAUTHORIZED } from "./auth.ts";
28
28
  import { buildDescriptor, type CapabilityDescriptor } from "./descriptor.ts";
29
29
  import { toWireEvent, type AgentsChatMessage } from "./normalize.ts";
30
- import { IdentityTable, routeInbound, type Identity } from "./identities.ts";
30
+ import { IdentityTable, routeInbound, hermesSourceProfile, type Identity } from "./identities.ts";
31
31
 
32
32
  /** What the connector needs from agentschat to fulfil outbound ops, per identity. */
33
33
  export interface AgentsChatHooks {
@@ -288,10 +288,8 @@ export function startConnector(config: ConnectorConfig): ConnectorHandle {
288
288
  log(`[connector] inbound unaddressed (channel=${(msg as any).channel_id ?? "?"} mentions=${JSON.stringify((msg as any).mentioned_ids ?? [])}) — dropped`);
289
289
  return;
290
290
  }
291
- const event = toWireEvent(msg, "agentschat");
292
- if (!event) return;
293
- // Tag the fronting identity so the gateway keys the right session/profile.
294
- (event.source as any).profile = target.botId;
291
+ const baseEvent = toWireEvent(msg, "agentschat");
292
+ if (!baseEvent) return;
295
293
  if (!isDm) {
296
294
  // Attach "what happened since you were last addressed" so the agent gets
297
295
  // the conversation BETWEEN its @-mentions without being injected into
@@ -304,7 +302,7 @@ export function startConnector(config: ConnectorConfig): ConnectorHandle {
304
302
  try {
305
303
  const ctx = await hooks.getChannelContext(target.botId, msg.channel_id!, since, msg.id);
306
304
  if (ctx && ctx.length) {
307
- event.context = ctx.slice(-10).map((c) => ({
305
+ baseEvent.context = ctx.slice(-10).map((c) => ({
308
306
  text: String(c?.text ?? "").slice(0, 500),
309
307
  source: { user_name: c?.user_name, user_id: c?.user_id },
310
308
  }));
@@ -316,7 +314,13 @@ export function startConnector(config: ConnectorConfig): ConnectorHandle {
316
314
  if (msg.timestamp) lastAddressed.set(key, msg.timestamp);
317
315
  }
318
316
  for (const conn of sockets) {
319
- if (conn.fronted.has(target.botId)) send(conn.ws, { type: "inbound", event });
317
+ if (!conn.fronted.has(target.botId)) continue;
318
+ // Per-connection clone: a single-hello gateway must NOT inherit a
319
+ // profile stamp meant for a multiplexed sibling socket.
320
+ const event = { ...baseEvent, source: { ...baseEvent.source } };
321
+ const profile = hermesSourceProfile(target, conn.fronted.size);
322
+ if (profile) (event.source as any).profile = profile;
323
+ send(conn.ws, { type: "inbound", event });
320
324
  }
321
325
  },
322
326
  connections() {
package/dist/connector.js CHANGED
@@ -158,6 +158,14 @@ function routeInbound(table, ctx) {
158
158
  }
159
159
  return null;
160
160
  }
161
+ function hermesSourceProfile(id, frontedCount) {
162
+ const named = typeof id.profile === "string" ? id.profile.trim() : "";
163
+ if (named)
164
+ return named;
165
+ if (frontedCount > 1)
166
+ return id.botId;
167
+ return;
168
+ }
161
169
 
162
170
  // connector/server.ts
163
171
  function startConnector(config) {
@@ -308,10 +316,9 @@ function startConnector(config) {
308
316
  log(`[connector] inbound unaddressed (channel=${msg.channel_id ?? "?"} mentions=${JSON.stringify(msg.mentioned_ids ?? [])}) — dropped`);
309
317
  return;
310
318
  }
311
- const event = toWireEvent(msg, "agentschat");
312
- if (!event)
319
+ const baseEvent = toWireEvent(msg, "agentschat");
320
+ if (!baseEvent)
313
321
  return;
314
- event.source.profile = target.botId;
315
322
  if (!isDm) {
316
323
  const key = `${target.botId}:${msg.channel_id}`;
317
324
  const since = lastAddressed.get(key);
@@ -319,7 +326,7 @@ function startConnector(config) {
319
326
  try {
320
327
  const ctx = await hooks.getChannelContext(target.botId, msg.channel_id, since, msg.id);
321
328
  if (ctx && ctx.length) {
322
- event.context = ctx.slice(-10).map((c) => ({
329
+ baseEvent.context = ctx.slice(-10).map((c) => ({
323
330
  text: String(c?.text ?? "").slice(0, 500),
324
331
  source: { user_name: c?.user_name, user_id: c?.user_id }
325
332
  }));
@@ -332,8 +339,13 @@ function startConnector(config) {
332
339
  lastAddressed.set(key, msg.timestamp);
333
340
  }
334
341
  for (const conn of sockets) {
335
- if (conn.fronted.has(target.botId))
336
- send(conn.ws, { type: "inbound", event });
342
+ if (!conn.fronted.has(target.botId))
343
+ continue;
344
+ const event = { ...baseEvent, source: { ...baseEvent.source } };
345
+ const profile = hermesSourceProfile(target, conn.fronted.size);
346
+ if (profile)
347
+ event.source.profile = profile;
348
+ send(conn.ws, { type: "inbound", event });
337
349
  }
338
350
  },
339
351
  connections() {
@@ -398,7 +410,8 @@ function resolveIdentities() {
398
410
  agentId: String(it.agentId ?? it.botId),
399
411
  token: String(it.token),
400
412
  gatewayId: String(it.gatewayId),
401
- secret: String(it.secret)
413
+ secret: String(it.secret),
414
+ ...it.profile ? { profile: String(it.profile) } : {}
402
415
  }));
403
416
  }
404
417
  const agentId = need("AGENTCHAT_AGENT_ID");
@@ -416,6 +429,33 @@ for (const id of identities) {
416
429
  var broadcast = null;
417
430
  var socketsByBot = new Map;
418
431
  var backoffByBot = new Map;
432
+ function joinChannel(ws, id, channelId, name) {
433
+ try {
434
+ ws.send(JSON.stringify({ type: "join_channel", channel_id: channelId, agent_id: id.agentId }));
435
+ log(`joined channel ${channelId}${name ? ` (${name})` : ""}`);
436
+ } catch (e) {
437
+ log(`join ${channelId} failed: ${e?.message ?? e}`);
438
+ }
439
+ }
440
+ async function joinMemberships(ws, id) {
441
+ try {
442
+ const r = await fetch(`${API}/api/channels/mine`, { headers: { Authorization: `Bearer ${id.token}` } });
443
+ if (!r.ok) {
444
+ log(`list mine failed: ${r.status}`);
445
+ return;
446
+ }
447
+ const body = await r.json();
448
+ const channels = Array.isArray(body) ? body : body.channels || [];
449
+ for (const ch of channels) {
450
+ const channelId = ch?.id || ch?.channel_id;
451
+ if (!channelId)
452
+ continue;
453
+ joinChannel(ws, id, channelId, ch?.name);
454
+ }
455
+ } catch (e) {
456
+ log(`join-on-auth failed: ${e?.message ?? e}`);
457
+ }
458
+ }
419
459
  function connectIdentity(id) {
420
460
  const ws = new WS(WS_URL);
421
461
  socketsByBot.set(id.botId, ws);
@@ -434,6 +474,11 @@ function connectIdentity(id) {
434
474
  }
435
475
  if (data.type === "auth_ok") {
436
476
  log(`connected to agentschat as ${id.agentId}`);
477
+ joinMemberships(ws, id);
478
+ return;
479
+ }
480
+ if (data.type === "channel_created" && data.channel_id) {
481
+ joinChannel(ws, id, data.channel_id, data.name);
437
482
  return;
438
483
  }
439
484
  if (data.type === "message" && data.sender_id !== id.agentId) {
package/dist/server.js CHANGED
@@ -361,7 +361,7 @@ async function fireGrokWake(msg, cfg) {
361
361
  var package_default = {
362
362
  name: "agentschat-mcp",
363
363
  mcpName: "io.github.swswordholy-tech/agentschat-mcp",
364
- version: "0.33.0",
364
+ version: "0.33.2",
365
365
  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.",
366
366
  type: "module",
367
367
  bin: {
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.33.0",
4
+ "version": "0.33.2",
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": {