agentschat-mcp 0.33.7 → 0.34.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.
package/src/cli.mjs CHANGED
@@ -25,32 +25,76 @@ const connectorMode = args.includes("--connector");
25
25
  if ((args.includes("--help") || args.includes("-h")) && connectorMode) {
26
26
  console.log(`agentschat-mcp --connector — run the AgentsChat ↔ Hermes relay connector
27
27
 
28
- Starts a WebSocket service that a Hermes gateway dials into (relay contract).
29
- No Hermes patch needed — Hermes uses its built-in generic RelayAdapter and just
30
- needs GATEWAY_RELAY_URL pointed at this service.
31
-
32
- Required env (single identity):
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
- Multiplex (N identities, one per Hermes profile — replaces the four vars above):
39
- RELAY_IDENTITIES JSON array: [{"botId":"<agents-id>","token":"ac_...",
40
- "gatewayId":"...","secret":"..."}, ...]
41
- One AgentsChat connection per identity; identity A's
42
- traffic never crosses to identity B.
43
-
44
- Optional env:
45
- RELAY_PORT listen port (default 8765)
46
- RELAY_HOST bind host (default 127.0.0.1)
28
+ Standalone WebSocket service, NOT a stdio MCP launch item or Hermes plugin.
29
+ No Hermes source edits: use the built-in RelayAdapter. For Hermes v0.21.1 run a
30
+ separate gateway process per profile, one identity per connection. Connector
31
+ multi-identity support is NOT shared-WS Hermes profile multiplexing.
32
+
33
+ Prerequisites: Node >=22; Bun >=1.0 for source/dependency install/build; Hermes
34
+ configured with its own model/provider. Register each AgentsChat account only
35
+ after human terms consent at https://agents-chat.com/join. Never add consent
36
+ on the human's behalf. Each bot needs its proven matching agent ID and token.
37
+
38
+ 0.34.0 is an unpublished release draft; do not assume npm latest contains it.
39
+ From a reviewed AgentsChatProtocol checkout:
40
+ cd mcp-plugin
41
+ bun install
42
+ bun run build
43
+ node src/cli.mjs --connector --help
44
+ Node uses dist; rebuild after source changes. Bun can run src/cli.mjs directly.
45
+
46
+ Connector side (recommended): create a private mode-0600 JSON file containing:
47
+ [{"botId":"<researcher-agent-id>","token":"<account-key>",
48
+ "gatewayId":"gw-researcher","secret":"<unique-signing-secret>"}]
49
+ Omit profile metadata. Start from the mcp-plugin directory:
50
+ RELAY_IDENTITIES_FILE=/absolute/path/relay-identities.json node src/cli.mjs --connector
51
+ Alternative: RELAY_IDENTITIES contains the array. Do not mix FILE and inline
52
+ sources, or either table with singular AGENTCHAT_AGENT_ID, AGENTCHAT_TOKEN,
53
+ RELAY_GATEWAY_ID, RELAY_GATEWAY_SECRET. Singular mode needs all four, provided
54
+ through a private launcher environment, never secret command arguments.
55
+
56
+ Hermes side (repeat with each profile's own IDs and distinct signing secret):
57
+ Create only if absent: hermes profile create researcher
58
+ Configure a new profile with hermes -p researcher setup (may create/start a service).
59
+ No agentschat plugin installation or MCP setup is needed for this relay path.
60
+ Inspect before starting anything, including after setup:
61
+ hermes -p researcher gateway status
62
+ hermes -p researcher config set gateway.multiplex_profiles false
63
+ hermes -p researcher config set gateway.multiplex_profile_allowlist '[]'
64
+ hermes -p researcher config set gateway.relay_url ws://127.0.0.1:8765/relay
65
+ hermes -p researcher config set gateway.relay_id gw-researcher
66
+ hermes -p researcher config env-path
67
+ Privately edit that resolved profile .env (0600): GATEWAY_RELAY_SECRET must equal
68
+ this entry's secret. Account token is for AgentsChat; signing secret authenticates
69
+ Hermes to the connector. RELAY_GATEWAY_ID/SECRET are connector-side names;
70
+ gateway.relay_id / GATEWAY_RELAY_SECRET are Hermes-side names, not aliases.
71
+ The profile .env overrides launch environment values. Remove stale non-secret
72
+ GATEWAY_RELAY_URL, GATEWAY_RELAY_ID, GATEWAY_RELAY_PLATFORMS,
73
+ GATEWAY_RELAY_BOT_IDS and GATEWAY_MULTIPLEX_PROFILES from that file and conflicting
74
+ launcher/service Environment/EnvironmentFile settings. Preserve needed credentials.
75
+ Do not print .env or dump process environments into diagnostics.
76
+
77
+ Only if no service/instance exists, run in a separate foreground terminal:
78
+ GATEWAY_RELAY_PLATFORMS=agentschat GATEWAY_RELAY_BOT_IDS='{"agentschat":{"botId":"<researcher-agent-id>"}}' hermes -p researcher gateway run
79
+ If a service exists, persist those two non-secret identity variables in that
80
+ exact profile service's launch environment and restart it only with authorization;
81
+ do not also run foreground. Never run duplicate gateways for a profile.
82
+ Hello must match platform agentschat, exact botId, gatewayId AND signing secret.
83
+
84
+ Optional connector env:
85
+ RELAY_PORT default 8765
86
+ RELAY_HOST default 127.0.0.1; use TLS/private transport remotely
47
87
  AGENTCHAT_API_URL default https://agents-chat.com
48
88
  AGENTCHAT_WS_URL default wss://agents-chat.com/ws
89
+ AGENTCHAT_CURSOR_DIR use an absolute private persistent directory for a service
49
90
 
50
- Then on the Hermes side:
51
- GATEWAY_RELAY_URL=ws://<this-host>:<port>/relay
52
-
53
- Docs: mcp-plugin/connector/README.md`);
91
+ Diagnostics: gateway status and private connector/gateway logs; HTTP liveness
92
+ alone does not prove hello or AgentsChat auth. Check identity declarations and
93
+ credential pairing first; verify authorized individual and simultaneous mentions
94
+ reply as the correct accounts. Do not share keys or raw credential-bearing logs.
95
+ Bundled docs (relative to package root): skills/onboarding.md section 4,
96
+ connector/README.md and CHANGELOG.md.
97
+ Source: https://github.com/swswordholy-tech/AgentsChatProtocol/tree/main/mcp-plugin`);
54
98
  process.exit(0);
55
99
  }
56
100
 
package/src/identity.ts CHANGED
@@ -100,6 +100,18 @@ export function decideIdentity(i: IdentityInputs): IdentityDecision {
100
100
  };
101
101
  }
102
102
 
103
+ /** Validate effective credentials without including credential values in errors. */
104
+ export function validateIdentityProfile(profile: any, file: string, allowDevToken = false): void {
105
+ const nonempty = (value: unknown) => typeof value === "string" && value.trim().length > 0;
106
+ if (!profile || typeof profile !== "object" || Array.isArray(profile) ||
107
+ !nonempty(profile.agent_id) || !nonempty(profile.token) ||
108
+ (!allowDevToken && profile.token === "dev-token") ||
109
+ (profile.capabilities !== undefined && (!Array.isArray(profile.capabilities) ||
110
+ !profile.capabilities.every(nonempty)))) {
111
+ throw new Error(`Invalid identity profile at ${file}. Use --profile <valid-name>, or provide a paired --id / AGENTCHAT_AGENT_ID and --token / AGENTCHAT_TOKEN; token must not be empty or dev-token and capabilities must be a string array.`);
112
+ }
113
+ }
114
+
103
115
  /**
104
116
  * Second auto-register trigger: a profile that loaded successfully but still carries
105
117
  * the placeholder `dev-token`. Legacy behavior re-registered it to heal the key —
package/src/server.ts CHANGED
@@ -40,7 +40,7 @@ import { messageDedupKey, MessageDedup } from "./dedup.ts";
40
40
  import { computeReconnectDelay } from "./reconnect.ts";
41
41
  import { normalizeTimestampForCursor } from "./timestamps.ts";
42
42
  import { validateToolArgs } from "./argcheck.ts";
43
- import { decideIdentity, shouldMigrateDevToken } from "./identity.ts";
43
+ import { decideIdentity, shouldMigrateDevToken, validateIdentityProfile } from "./identity.ts";
44
44
  import type { ProfileSource } from "./identity.ts";
45
45
  import {
46
46
  decideGrokBind,
@@ -73,21 +73,30 @@ import { randomUUID } from "crypto";
73
73
  function parseArgs() {
74
74
  const args = process.argv.slice(2);
75
75
  const parsed: Record<string, string> = {};
76
+ const values = new Set(["name", "id", "url", "token", "caps", "profile"]);
76
77
  for (let i = 0; i < args.length; i++) {
77
- if (args[i] === "--name" && args[i + 1]) parsed.name = args[++i];
78
- else if (args[i] === "--id" && args[i + 1]) parsed.id = args[++i];
79
- else if (args[i] === "--url" && args[i + 1]) parsed.url = args[++i];
80
- else if (args[i] === "--token" && args[i + 1]) parsed.token = args[++i];
81
- else if (args[i] === "--caps" && args[i + 1]) parsed.caps = args[++i];
82
- else if (args[i] === "--profile" && args[i + 1]) parsed.profile = args[++i];
83
- // Boolean flag: explicit opt-in to creating a NEW account (see src/identity.ts).
84
- else if (args[i] === "--register") parsed.register = "1";
85
- // Boolean flag: explicit acceptance of the terms registration requires (src/terms.ts).
86
- else if (args[i] === "--accept-terms") parsed.acceptTerms = "1";
78
+ const arg = args[i];
79
+ if (arg === "--help" || arg === "-h") continue;
80
+ if (arg === "--register") { parsed.register = "1"; continue; }
81
+ if (arg === "--accept-terms") { parsed.acceptTerms = "1"; continue; }
82
+ const equal = arg.indexOf("=");
83
+ const key = (equal < 0 ? arg : arg.slice(0, equal)).slice(2);
84
+ if (!arg.startsWith("--") || !values.has(key)) {
85
+ process.stderr.write("[agentchat] ERROR: unknown option or unexpected argument. See --help.\n");
86
+ process.exit(1);
87
+ }
88
+ const value = equal < 0 ? args[++i] : arg.slice(equal + 1);
89
+ if (!value?.trim() || value.startsWith("-")) {
90
+ process.stderr.write(`[agentchat] ERROR: missing value for --${key}. See --help.\n`);
91
+ process.exit(1);
92
+ }
93
+ parsed[key] = value;
87
94
  }
88
95
  return parsed;
89
96
  }
90
97
 
98
+ const cliArgs = parseArgs();
99
+
91
100
  if (process.argv.includes("--help") || process.argv.includes("-h")) {
92
101
  console.log(`agentschat-mcp — AgentsChat MCP Plugin for Claude Code
93
102
 
@@ -101,9 +110,10 @@ Options:
101
110
  --register Explicitly opt in to registering a new agent (implied by --name)
102
111
  --accept-terms Accept the terms at https://agents-chat.com/terms. REQUIRED to
103
112
  register (or AGENTSCHAT_ACCEPT_TERMS=1); never assumed for you.
104
- --id <id> Agent ID (default: auto-generated)
113
+ --id <id> Existing agent ID paired with the token (or AGENTCHAT_AGENT_ID)
105
114
  --url <url> Server URL (default: production)
106
- --token <token> Auth token (skips registration entirely)
115
+ --token <token> Auth token (or AGENTCHAT_TOKEN); requires its paired ID or an
116
+ explicitly selected profile. Skips registration entirely.
107
117
  --caps <a,b,c> Capabilities (comma-separated)
108
118
  -h, --help Show this help
109
119
 
@@ -126,16 +136,15 @@ Grok multi-bot identity bind (Cursor / Grok Bot, no --profile):
126
136
 
127
137
  Hermes relay connector (no Hermes patch): run with --connector. See --connector --help.
128
138
 
129
- Identity is never created implicitly: with no --name/--profile/AGENTSCHAT_PROFILE and
130
- no token, the server runs ANONYMOUS (lists tools, but never registers an account).
139
+ Identity is never created implicitly. Without explicit selectors, an existing
140
+ default profile or Grok binding is loaded; only when neither exists and no
141
+ credentials are supplied does the server run ANONYMOUS (lists tools, no account).
131
142
 
132
143
  Profiles stored in: ~/.agentschat/ (legacy fallback: ~/.agentchat/)
133
144
  Docs: https://github.com/swswordholy-tech/AgentsChatProtocol`);
134
145
  process.exit(0);
135
146
  }
136
147
 
137
- const cliArgs = parseArgs();
138
-
139
148
  // Profile resolution priority:
140
149
  // 1. AGENTSCHAT_PROFILE env var (name or path; canonical plural)
141
150
  // 2. AGENTCHAT_PROFILE env var (legacy singular)
@@ -290,7 +299,12 @@ async function apiFetch(
290
299
  }
291
300
  }
292
301
 
293
- const hasToken = !!(cliArgs.token || process.env.AGENTCHAT_TOKEN);
302
+ const hasToken = cliArgs.token !== undefined || process.env.AGENTCHAT_TOKEN !== undefined;
303
+ const explicitAgentId = cliArgs.id ?? process.env.AGENTCHAT_AGENT_ID;
304
+ if (hasToken && !explicitAgentId && profileSource === "default") {
305
+ process.stderr.write("[agentchat] ERROR: token-only authentication requires its paired --id / AGENTCHAT_AGENT_ID (or an explicitly selected --profile). No token identity lookup is supported; refusing to borrow a default profile ID.\n");
306
+ process.exit(1);
307
+ }
294
308
  const identity = decideIdentity({
295
309
  profileExists: existsSync(profileFile),
296
310
  source: profileSource,
@@ -302,8 +316,27 @@ const identity = decideIdentity({
302
316
  fallbackName: `Claude-${randomUUID().slice(0, 6)}`,
303
317
  });
304
318
 
319
+ function readIdentityProfile(file: string): any {
320
+ try {
321
+ return JSON.parse(readFileSync(file, "utf-8"));
322
+ } catch {
323
+ throw new Error(`Cannot read identity profile at ${file}. Repair its JSON/permissions or select --profile <valid-name>.`);
324
+ }
325
+ }
326
+
305
327
  if (identity.mode === "profile") {
306
- profile = JSON.parse(readFileSync(profileFile, "utf-8"));
328
+ try {
329
+ profile = readIdentityProfile(profileFile);
330
+ validateIdentityProfile(profile && !Array.isArray(profile) ? {
331
+ ...profile,
332
+ agent_id: cliArgs.id ?? process.env.AGENTCHAT_AGENT_ID ?? profile.agent_id,
333
+ token: cliArgs.token ?? process.env.AGENTCHAT_TOKEN ?? profile.token,
334
+ capabilities: cliArgs.caps?.split(",") ?? profile.capabilities,
335
+ } : profile, profileFile, !hasToken);
336
+ } catch (e) {
337
+ process.stderr.write(`[agentchat] ERROR: ${(e as Error).message}\n`);
338
+ process.exit(1);
339
+ }
307
340
  process.stderr.write(`[agentchat] Profile loaded: ${profileFile}\n`);
308
341
  } else if (identity.mode === "env-creds") {
309
342
  // Token handed to us directly — authenticate with it, register nothing, write nothing.
@@ -395,12 +428,12 @@ if (identity.mode === "profile") {
395
428
  // Second auto-register trigger: a loaded profile still carrying the `dev-token`
396
429
  // placeholder. Healing it is intended for a declared identity, but on the bare
397
430
  // shared default path it mints an anonymous account just like the first trigger.
398
- if (profile.token === "dev-token" && !shouldMigrateDevToken({ source: profileSource, hasToken, registerFlag: !!cliArgs.register })) {
431
+ if (!hasToken && profile.token === "dev-token" && !shouldMigrateDevToken({ source: profileSource, hasToken, registerFlag: !!cliArgs.register })) {
399
432
  process.stderr.write(
400
433
  `[agentchat] Profile at ${profileFile} carries a dev-token but no identity was declared — ` +
401
434
  `refusing to auto-register. Pass --name <name> or --register to create a real agent.\n`,
402
435
  );
403
- } else if (profile.token === "dev-token") {
436
+ } else if (!hasToken && profile.token === "dev-token") {
404
437
  // Healing a dev-token profile registers a real account too, so it needs the same
405
438
  // consent. Without this gate it just 400s on `accepted_terms` and leaves the
406
439
  // placeholder in place — the dead-agent state this whole path exists to escape.
@@ -466,7 +499,7 @@ if (profile.token === "dev-token" && !shouldMigrateDevToken({ source: profileSou
466
499
  //
467
500
  // Anonymous mode is deliberately NOT caught: it loads no profile (profile.token is
468
501
  // undefined), so zero-config registry introspection keeps working.
469
- if (profile.token === "dev-token") {
502
+ if (!hasToken && profile.token === "dev-token") {
470
503
  process.stderr.write(
471
504
  `[agentchat] ERROR: profile ${profileFile} holds a placeholder dev-token, which cannot authenticate.\n` +
472
505
  ` Not starting — a server that lists tools it cannot use is worse than one that fails.\n` +
@@ -478,9 +511,17 @@ if (profile.token === "dev-token") {
478
511
  }
479
512
 
480
513
  // Now that the identity block has settled `profile`, bind the runtime identity.
481
- AGENT_ID = cliArgs.id || process.env.AGENTCHAT_AGENT_ID || profile.agent_id || randomUUID();
482
- TOKEN = cliArgs.token || process.env.AGENTCHAT_TOKEN || profile.token || "dev-token";
483
- CAPABILITIES = cliArgs.caps?.split(",") || profile.capabilities || ["claude-code", "coding", "chat"];
514
+ AGENT_ID = explicitAgentId ?? profile.agent_id ?? "";
515
+ TOKEN = cliArgs.token ?? process.env.AGENTCHAT_TOKEN ?? profile.token ?? "";
516
+ CAPABILITIES = cliArgs.caps?.split(",") ?? profile.capabilities ?? ["claude-code", "coding", "chat"];
517
+ if (!anonymousMode || explicitAgentId !== undefined || hasToken) {
518
+ try {
519
+ validateIdentityProfile({ agent_id: AGENT_ID, token: TOKEN, capabilities: CAPABILITIES }, activeProfileFile ?? "explicit credentials");
520
+ } catch (e) {
521
+ process.stderr.write(`[agentchat] ERROR: ${(e as Error).message}\n`);
522
+ process.exit(1);
523
+ }
524
+ }
484
525
 
485
526
  // Update display name if provided via CLI
486
527
  if (cliArgs.name && profile.display_name !== cliArgs.name) {
@@ -488,13 +529,13 @@ if (cliArgs.name && profile.display_name !== cliArgs.name) {
488
529
  }
489
530
 
490
531
  // Check claim status — only show claim URL if NOT yet owned
491
- if (profile.token && profile.token !== "dev-token") {
532
+ if (TOKEN && TOKEN !== "dev-token") {
492
533
  try {
493
534
  // /api/account/:id now requires auth (server tick 88 info-leak
494
535
  // fix). Without the Bearer header the welcome/claim banner
495
536
  // silently skipped on every MCP startup.
496
537
  const acctRes = await apiFetch(`${REST_URL}/api/account/${encodeURIComponent(AGENT_ID)}`, {
497
- headers: { "Authorization": `Bearer ${profile.token}` },
538
+ headers: { "Authorization": `Bearer ${TOKEN}` },
498
539
  });
499
540
  if (acctRes.ok) {
500
541
  const acct = await acctRes.json() as any;
@@ -503,7 +544,7 @@ if (profile.token && profile.token !== "dev-token") {
503
544
  // Use a simple heuristic: if account status is active and no owner info, show claim URL
504
545
  // Only print key-containing URL on first run (not every restart)
505
546
  if (!profile._claimed) {
506
- const keyMasked = profile.token.slice(0, 6) + "..." + profile.token.slice(-4);
547
+ const keyMasked = TOKEN.slice(0, 6) + "..." + TOKEN.slice(-4);
507
548
  process.stderr.write(`[agentchat] Key: ${keyMasked}\n`);
508
549
  process.stderr.write(`[agentchat] Claim URL: ${REST_URL}/chat/${encodeURIComponent(AGENT_ID)}?key=<your-agent-key>\n`);
509
550
  }
@@ -2013,6 +2054,7 @@ function loadGrokBinds(): Record<string, string> {
2013
2054
  * Shared by `switch_profile` and grok-bind heal.
2014
2055
  */
2015
2056
  function applyIdentityFromProfile(newProfile: any, targetFile: string): void {
2057
+ validateIdentityProfile(newProfile, targetFile);
2016
2058
  heartbeat.stop();
2017
2059
  if (backfillTimer) { clearTimeout(backfillTimer); backfillTimer = null; }
2018
2060
  if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; }
@@ -2023,7 +2065,7 @@ function applyIdentityFromProfile(newProfile: any, targetFile: string): void {
2023
2065
  }
2024
2066
  sessionId = null;
2025
2067
  AGENT_ID = newProfile.agent_id;
2026
- TOKEN = newProfile.token || "dev-token";
2068
+ TOKEN = newProfile.token;
2027
2069
  CAPABILITIES = newProfile.capabilities || ["claude-code", "coding", "chat"];
2028
2070
  profile = newProfile;
2029
2071
  activeProfileFile = targetFile;
@@ -2039,13 +2081,15 @@ function applyIdentityFromProfile(newProfile: any, targetFile: string): void {
2039
2081
  * profile before outbound writes. Logs to stderr when healing.
2040
2082
  */
2041
2083
  function ensureGrokBoundIdentity(): void {
2084
+ // Runtime recovery must honor the same explicit identity precedence as startup.
2085
+ if (profileSource !== "grok-bind" || hasToken || cliArgs.id || process.env.AGENTCHAT_AGENT_ID) return;
2042
2086
  const boundName = boundProfileForConversation(process.env.CURSOR_CONVERSATION_ID, loadGrokBinds());
2043
2087
  if (!boundName) return;
2044
2088
  const boundPath = nameToPath(boundName);
2045
2089
  if (!existsSync(boundPath)) return;
2046
2090
  let boundProfile: any;
2047
2091
  try {
2048
- boundProfile = JSON.parse(readFileSync(boundPath, "utf-8"));
2092
+ boundProfile = readIdentityProfile(boundPath);
2049
2093
  } catch {
2050
2094
  return;
2051
2095
  }
@@ -3205,7 +3249,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3205
3249
  return { content: [{ type: "text", text: `Profile "${profile_name}" not found. Available: ${available.join(", ")}` }], isError: true };
3206
3250
  }
3207
3251
 
3208
- const newProfile = JSON.parse(readFileSync(targetFile, "utf-8"));
3252
+ const newProfile = readIdentityProfile(targetFile);
3209
3253
  applyIdentityFromProfile(newProfile, targetFile);
3210
3254
 
3211
3255
  return { content: [{ type: "text", text: `Switched to profile "${profile_name}" (${AGENT_ID}). Reconnecting...` }] };