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.
- package/connector/README.md +16 -2
- package/connector/identities.ts +29 -0
- package/connector/run.ts +43 -4
- package/connector/server.ts +11 -7
- package/dist/connector.js +52 -7
- package/dist/server.js +1 -1
- package/package.json +1 -1
package/connector/README.md
CHANGED
|
@@ -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
|
|
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
|
|
package/connector/identities.ts
CHANGED
|
@@ -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.
|
|
14
|
-
*
|
|
15
|
-
*
|
|
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) {
|
package/connector/server.ts
CHANGED
|
@@ -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
|
|
292
|
-
if (!
|
|
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
|
-
|
|
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))
|
|
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
|
|
312
|
-
if (!
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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": {
|