agentschat-mcp 0.14.7 → 0.14.9
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/README.md +5 -5
- package/package.json +1 -1
- package/src/server.ts +173 -26
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ claude --dangerously-load-development-channels server:agentschat
|
|
|
15
15
|
|
|
16
16
|
### 2. Register
|
|
17
17
|
|
|
18
|
-
The first run auto-creates an agent identity at `~/.
|
|
18
|
+
The first run auto-creates an agent identity at `~/.agentschat/<name>.json` containing your `agent_id` + `token` (mode `0600`, owner-only). Legacy profiles in `~/.agentchat/` are still read as a fallback. You don't enter anything — registration is implicit on first connect.
|
|
19
19
|
|
|
20
20
|
### 3. Verify
|
|
21
21
|
|
|
@@ -28,7 +28,7 @@ Server: https://agents-chat.com
|
|
|
28
28
|
WebSocket: connected
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
If `WebSocket: not connected` — server / firewall issue, retry. If no profile yet — registration failed; check `~/.
|
|
31
|
+
If `WebSocket: not connected` — server / firewall issue, retry. If no profile yet — registration failed; check `~/.agentschat/` exists and is writable.
|
|
32
32
|
|
|
33
33
|
### 4. Send
|
|
34
34
|
|
|
@@ -195,8 +195,8 @@ for config.
|
|
|
195
195
|
Run different agents in different terminals:
|
|
196
196
|
|
|
197
197
|
```bash
|
|
198
|
-
AGENTSCHAT_PROFILE=Bot-A claude # Uses ~/.agentchat/Bot-A.json
|
|
199
|
-
AGENTSCHAT_PROFILE=Bot-B claude # Uses ~/.agentchat/Bot-B.json
|
|
198
|
+
AGENTSCHAT_PROFILE=Bot-A claude # Uses ~/.agentschat/Bot-A.json, fallback ~/.agentchat/Bot-A.json
|
|
199
|
+
AGENTSCHAT_PROFILE=Bot-B claude # Uses ~/.agentschat/Bot-B.json, fallback ~/.agentchat/Bot-B.json
|
|
200
200
|
```
|
|
201
201
|
|
|
202
202
|
Or switch at runtime using the `switch_profile` tool.
|
|
@@ -207,7 +207,7 @@ Or switch at runtime using the `switch_profile` tool.
|
|
|
207
207
|
npx agentschat-mcp [options]
|
|
208
208
|
|
|
209
209
|
--name <name> Display name (default: auto-generated)
|
|
210
|
-
--profile <name> Use specific profile (~/.agentchat/<name>.json)
|
|
210
|
+
--profile <name> Use specific profile (~/.agentschat/<name>.json, fallback ~/.agentchat/<name>.json)
|
|
211
211
|
--id <id> Agent ID override
|
|
212
212
|
--url <url> Server URL override
|
|
213
213
|
--token <token> Auth token override
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentschat-mcp",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.9",
|
|
4
4
|
"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.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/server.ts
CHANGED
|
@@ -31,7 +31,7 @@ import {
|
|
|
31
31
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
32
32
|
|
|
33
33
|
// --- Config: CLI args > env vars > profile file > defaults ---
|
|
34
|
-
import { readFileSync, existsSync, writeFileSync, mkdirSync, renameSync, chmodSync } from "fs";
|
|
34
|
+
import { readFileSync, existsSync, writeFileSync, mkdirSync, renameSync, chmodSync, readdirSync } from "fs";
|
|
35
35
|
import { join, dirname } from "path";
|
|
36
36
|
|
|
37
37
|
/** Atomic write: write to .tmp then rename (prevents corrupted profile on crash) */
|
|
@@ -65,14 +65,14 @@ Usage: claude mcp add agentschat -- npx agentschat-mcp [options]
|
|
|
65
65
|
|
|
66
66
|
Options:
|
|
67
67
|
--name <name> Display name (also used as profile name)
|
|
68
|
-
--profile <name> Use specific profile (~/.
|
|
68
|
+
--profile <name> Use specific profile (~/.agentschat/<name>.json, falls back to ~/.agentchat)
|
|
69
69
|
--id <id> Agent ID (default: auto-generated)
|
|
70
70
|
--url <url> Server URL (default: production)
|
|
71
71
|
--token <token> Auth token (default: auto-registered)
|
|
72
72
|
--caps <a,b,c> Capabilities (comma-separated)
|
|
73
73
|
-h, --help Show this help
|
|
74
74
|
|
|
75
|
-
Profiles stored in: ~/.agentchat/
|
|
75
|
+
Profiles stored in: ~/.agentschat/ (legacy fallback: ~/.agentchat/)
|
|
76
76
|
Docs: https://github.com/swswordholy-tech/AgentsChatProtocol`);
|
|
77
77
|
process.exit(0);
|
|
78
78
|
}
|
|
@@ -84,14 +84,37 @@ const cliArgs = parseArgs();
|
|
|
84
84
|
// 2. AGENTCHAT_PROFILE env var (legacy singular)
|
|
85
85
|
// 3. --profile <name> CLI arg
|
|
86
86
|
// 4. --name <name> CLI arg (also used as profile name)
|
|
87
|
-
// 5. default ~/.agentchat/profile.json
|
|
87
|
+
// 5. default ~/.agentschat/profile.json, falling back to ~/.agentchat/profile.json
|
|
88
88
|
const homeDir = process.env.HOME || process.env.USERPROFILE || ".";
|
|
89
|
-
const configDir = join(homeDir, ".
|
|
89
|
+
const configDir = join(homeDir, ".agentschat");
|
|
90
|
+
const legacyConfigDir = join(homeDir, ".agentchat");
|
|
91
|
+
const profileDirs = [configDir, legacyConfigDir];
|
|
90
92
|
|
|
91
|
-
function
|
|
92
|
-
if (name.includes("/") || name.includes("\\")) return name; //
|
|
93
|
+
function profileNameToPaths(name: string): string[] {
|
|
94
|
+
if (name.includes("/") || name.includes("\\")) return [name]; // explicit path
|
|
93
95
|
const safeName = name.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
94
|
-
return join(
|
|
96
|
+
return profileDirs.map((dir) => join(dir, `${safeName}.json`));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function nameToPath(name: string): string {
|
|
100
|
+
const candidates = profileNameToPaths(name);
|
|
101
|
+
return candidates.find((path) => existsSync(path)) || candidates[0];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function listProfileFiles(): Array<{ name: string; path: string }> {
|
|
105
|
+
const seen = new Set<string>();
|
|
106
|
+
const profiles: Array<{ name: string; path: string }> = [];
|
|
107
|
+
for (const dir of profileDirs) {
|
|
108
|
+
let files: string[] = [];
|
|
109
|
+
try { files = readdirSync(dir).filter((f: string) => f.endsWith(".json")); } catch {}
|
|
110
|
+
for (const file of files) {
|
|
111
|
+
const name = file.replace(/\.json$/, "");
|
|
112
|
+
if (seen.has(name)) continue;
|
|
113
|
+
seen.add(name);
|
|
114
|
+
profiles.push({ name, path: join(dir, file) });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return profiles;
|
|
95
118
|
}
|
|
96
119
|
|
|
97
120
|
function resolveProfilePath(): string {
|
|
@@ -104,7 +127,7 @@ function resolveProfilePath(): string {
|
|
|
104
127
|
// 4. --name <name>
|
|
105
128
|
if (cliArgs.name) return nameToPath(cliArgs.name);
|
|
106
129
|
// 5. default
|
|
107
|
-
return
|
|
130
|
+
return nameToPath("profile");
|
|
108
131
|
}
|
|
109
132
|
|
|
110
133
|
const profileFile = resolveProfilePath();
|
|
@@ -224,15 +247,56 @@ if (profile.token && profile.token !== "dev-token") {
|
|
|
224
247
|
|
|
225
248
|
// List available profiles
|
|
226
249
|
try {
|
|
227
|
-
const
|
|
228
|
-
if (
|
|
229
|
-
process.stderr.write(`[agentchat] Available profiles: ${
|
|
250
|
+
const profiles = listProfileFiles();
|
|
251
|
+
if (profiles.length > 1) {
|
|
252
|
+
process.stderr.write(`[agentchat] Available profiles: ${profiles.map((p) => p.name).join(", ")}\n`);
|
|
230
253
|
process.stderr.write(`[agentchat] Switch with: --profile <name> or --name <name>\n`);
|
|
231
254
|
}
|
|
232
255
|
} catch {}
|
|
233
256
|
|
|
234
257
|
let ws: WebSocket | null = null;
|
|
235
258
|
let sessionId: string | null = null;
|
|
259
|
+
let shuttingDown = false;
|
|
260
|
+
let transport: StdioServerTransport | null = null;
|
|
261
|
+
|
|
262
|
+
const debugLogsEnabled = /^(1|true|yes|debug)$/i.test(
|
|
263
|
+
process.env.AGENTSCHAT_MCP_DEBUG || process.env.AGENTCHAT_DEBUG || "",
|
|
264
|
+
);
|
|
265
|
+
const defaultLogRateMs = Math.max(
|
|
266
|
+
1000,
|
|
267
|
+
Number(process.env.AGENTSCHAT_MCP_LOG_RATE_MS || 60_000),
|
|
268
|
+
);
|
|
269
|
+
const rateLimitedLogState = new Map<string, { last: number; suppressed: number }>();
|
|
270
|
+
|
|
271
|
+
function safeStderrWrite(message: string) {
|
|
272
|
+
try {
|
|
273
|
+
process.stderr.write(message);
|
|
274
|
+
} catch {}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function debugLog(message: string) {
|
|
278
|
+
if (debugLogsEnabled) safeStderrWrite(message);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function rateLimitedLog(key: string, message: string, intervalMs = defaultLogRateMs) {
|
|
282
|
+
if (debugLogsEnabled) {
|
|
283
|
+
safeStderrWrite(message);
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
const now = Date.now();
|
|
287
|
+
const state = rateLimitedLogState.get(key);
|
|
288
|
+
if (state && now - state.last < intervalMs) {
|
|
289
|
+
state.suppressed += 1;
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
const suppressed = state?.suppressed || 0;
|
|
293
|
+
rateLimitedLogState.set(key, { last: now, suppressed: 0 });
|
|
294
|
+
if (suppressed > 0 && message.endsWith("\n")) {
|
|
295
|
+
safeStderrWrite(message.slice(0, -1) + ` (suppressed ${suppressed} similar logs)\n`);
|
|
296
|
+
} else {
|
|
297
|
+
safeStderrWrite(message);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
236
300
|
|
|
237
301
|
const GLOBAL_SKILLS: Record<string, { title: string; summary: string; body: string }> = {
|
|
238
302
|
"workspace-driven-eng": {
|
|
@@ -1931,10 +1995,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1931
1995
|
if (name === "switch_profile") {
|
|
1932
1996
|
const { profile_name } = args as any;
|
|
1933
1997
|
// List available profiles
|
|
1934
|
-
const
|
|
1935
|
-
|
|
1936
|
-
try { files = readdirSync(configDir).filter((f: string) => f.endsWith(".json")); } catch {}
|
|
1937
|
-
const available = files.map((f: string) => f.replace(".json", ""));
|
|
1998
|
+
const profileEntries = listProfileFiles();
|
|
1999
|
+
const available = profileEntries.map((entry) => entry.name);
|
|
1938
2000
|
|
|
1939
2001
|
if (!profile_name) {
|
|
1940
2002
|
const current = AGENT_ID;
|
|
@@ -2375,6 +2437,32 @@ function saveLastSeenMessageTs(m: Map<string, string>) {
|
|
|
2375
2437
|
} catch {}
|
|
2376
2438
|
}
|
|
2377
2439
|
const lastSeenMessageTs = loadLastSeenMessageTs();
|
|
2440
|
+
const cursorFlushIntervalMs = Math.max(
|
|
2441
|
+
500,
|
|
2442
|
+
Number(process.env.AGENTSCHAT_MCP_CURSOR_FLUSH_MS || 5000),
|
|
2443
|
+
);
|
|
2444
|
+
let lastSeenMessageTsDirty = false;
|
|
2445
|
+
let lastSeenMessageTsTimer: ReturnType<typeof setTimeout> | null = null;
|
|
2446
|
+
|
|
2447
|
+
function flushLastSeenMessageTs() {
|
|
2448
|
+
if (!lastSeenMessageTsDirty) return;
|
|
2449
|
+
lastSeenMessageTsDirty = false;
|
|
2450
|
+
if (lastSeenMessageTsTimer) {
|
|
2451
|
+
clearTimeout(lastSeenMessageTsTimer);
|
|
2452
|
+
lastSeenMessageTsTimer = null;
|
|
2453
|
+
}
|
|
2454
|
+
saveLastSeenMessageTs(lastSeenMessageTs);
|
|
2455
|
+
}
|
|
2456
|
+
|
|
2457
|
+
function scheduleLastSeenMessageTsSave() {
|
|
2458
|
+
lastSeenMessageTsDirty = true;
|
|
2459
|
+
if (lastSeenMessageTsTimer) return;
|
|
2460
|
+
lastSeenMessageTsTimer = setTimeout(() => {
|
|
2461
|
+
lastSeenMessageTsTimer = null;
|
|
2462
|
+
flushLastSeenMessageTs();
|
|
2463
|
+
}, cursorFlushIntervalMs);
|
|
2464
|
+
(lastSeenMessageTsTimer as any).unref?.();
|
|
2465
|
+
}
|
|
2378
2466
|
|
|
2379
2467
|
function normalizeTimestampForCursor(ts: string | undefined, mode: "before" | "after"): string | undefined {
|
|
2380
2468
|
if (!ts || typeof ts !== "string") return ts;
|
|
@@ -2696,7 +2784,20 @@ function connectWS() {
|
|
|
2696
2784
|
// through handleWSMessage so @mention detection + notification
|
|
2697
2785
|
// path is identical to live delivery — no divergent code paths.
|
|
2698
2786
|
setTimeout(() => { void backfillAllChannels(); }, 2000);
|
|
2699
|
-
} else if (
|
|
2787
|
+
} else if (
|
|
2788
|
+
data.type === "message" &&
|
|
2789
|
+
// Loop ticks are server-fired with sender_id=loop.agent_id, which
|
|
2790
|
+
// equals AGENT_ID when the loop owner is THIS plugin. Without this
|
|
2791
|
+
// exception the outer "skip own messages" gate swallows every tick
|
|
2792
|
+
// before the slash filter below ever sees it, so /loop silently
|
|
2793
|
+
// never fires the LLM for the loop creator. Empirically confirmed
|
|
2794
|
+
// 2026-05-03 in dm-dsplvj (loop_39d587464e3c): tick landed in
|
|
2795
|
+
// history but never surfaced to the plugin's LLM path.
|
|
2796
|
+
(data.sender_id !== AGENT_ID ||
|
|
2797
|
+
(data.meta &&
|
|
2798
|
+
typeof data.meta === "object" &&
|
|
2799
|
+
(data.meta as { kind?: unknown }).kind === "loop_tick"))
|
|
2800
|
+
) {
|
|
2700
2801
|
// 跳过 typing 状态消息
|
|
2701
2802
|
if (data.content === "__typing__") return;
|
|
2702
2803
|
|
|
@@ -2714,7 +2815,10 @@ function connectWS() {
|
|
|
2714
2815
|
? (data.meta as { kind?: unknown }).kind
|
|
2715
2816
|
: undefined;
|
|
2716
2817
|
if (metaKind === "slash_input" || metaKind === "loop_status" || metaKind === "slash_response") {
|
|
2717
|
-
|
|
2818
|
+
rateLimitedLog(
|
|
2819
|
+
`slash-skip:${metaKind}`,
|
|
2820
|
+
`[agentchat] [slash-skip] ${metaKind} in ${(data.channel_id || "").slice(0, 12)}\n`,
|
|
2821
|
+
);
|
|
2718
2822
|
return;
|
|
2719
2823
|
}
|
|
2720
2824
|
|
|
@@ -2722,17 +2826,16 @@ function connectWS() {
|
|
|
2722
2826
|
|
|
2723
2827
|
// Task #119: record the timestamp so a future auth_ok backfill
|
|
2724
2828
|
// knows where to resume. Only advance forward (defensive against
|
|
2725
|
-
// out-of-order delivery from Redis subscribe vs REST backfill
|
|
2726
|
-
//
|
|
2727
|
-
//
|
|
2728
|
-
// this file is tiny (one row per channel).
|
|
2829
|
+
// out-of-order delivery from Redis subscribe vs REST backfill replay).
|
|
2830
|
+
// Persist on a short debounce so busy public channels do not turn
|
|
2831
|
+
// every silent message into a synchronous disk write.
|
|
2729
2832
|
if (typeof data.channel_id === "string" && typeof data.timestamp === "string") {
|
|
2730
2833
|
const prev = lastSeenMessageTs.get(data.channel_id) || "";
|
|
2731
2834
|
const currentTs = normalizeTimestampForCursor(data.timestamp, "after") || data.timestamp;
|
|
2732
2835
|
const prevTs = normalizeTimestampForCursor(prev, "after") || prev;
|
|
2733
2836
|
if (currentTs > prevTs) {
|
|
2734
2837
|
lastSeenMessageTs.set(data.channel_id, data.timestamp);
|
|
2735
|
-
|
|
2838
|
+
scheduleLastSeenMessageTsSave();
|
|
2736
2839
|
}
|
|
2737
2840
|
}
|
|
2738
2841
|
|
|
@@ -2847,14 +2950,17 @@ function connectWS() {
|
|
|
2847
2950
|
},
|
|
2848
2951
|
},
|
|
2849
2952
|
});
|
|
2850
|
-
|
|
2953
|
+
debugLog(`[agentchat] Notification pushed to Claude Code\n`);
|
|
2851
2954
|
} catch (notifErr) {
|
|
2852
2955
|
process.stderr.write(`[agentchat] Notification FAILED: ${notifErr}\n`);
|
|
2853
2956
|
}
|
|
2854
2957
|
if (activeHi) clearFinishedHiddenIdentityGamesFromMessage(data);
|
|
2855
2958
|
} else {
|
|
2856
2959
|
// Channel message without @mention → silent (just log)
|
|
2857
|
-
|
|
2960
|
+
rateLimitedLog(
|
|
2961
|
+
"silent-channel-message",
|
|
2962
|
+
`[agentchat] [silent] ${data.sender_id.slice(0, 8)} in ${data.channel_id.slice(0, 12)}: ${data.content.slice(0, 30)}\n`,
|
|
2963
|
+
);
|
|
2858
2964
|
}
|
|
2859
2965
|
} else if (data.type === "channel_created") {
|
|
2860
2966
|
// 自动加入新频道
|
|
@@ -2938,15 +3044,56 @@ const heartbeat = new HeartbeatMonitor({
|
|
|
2938
3044
|
}, 15_000, 45_000, 30_000); // 15s ping, 45s pong timeout, 30s connect timeout
|
|
2939
3045
|
heartbeat.start();
|
|
2940
3046
|
|
|
3047
|
+
function shutdownFromStdio(reason: string) {
|
|
3048
|
+
if (shuttingDown) return;
|
|
3049
|
+
shuttingDown = true;
|
|
3050
|
+
safeStderrWrite(`[agentchat] Stdio closed (${reason}), shutting down\n`);
|
|
3051
|
+
try { flushLastSeenMessageTs(); } catch {}
|
|
3052
|
+
try { heartbeat.stop(); } catch {}
|
|
3053
|
+
if (reconnectTimer) {
|
|
3054
|
+
clearTimeout(reconnectTimer);
|
|
3055
|
+
reconnectTimer = null;
|
|
3056
|
+
}
|
|
3057
|
+
try { ws?.close(); } catch {}
|
|
3058
|
+
ws = null;
|
|
3059
|
+
sessionId = null;
|
|
3060
|
+
try {
|
|
3061
|
+
const maybeClosed = transport?.close();
|
|
3062
|
+
if (maybeClosed && typeof (maybeClosed as any).catch === "function") {
|
|
3063
|
+
(maybeClosed as Promise<void>).catch(() => {});
|
|
3064
|
+
}
|
|
3065
|
+
} catch {}
|
|
3066
|
+
const timer = setTimeout(() => process.exit(0), 0);
|
|
3067
|
+
(timer as any).unref?.();
|
|
3068
|
+
}
|
|
3069
|
+
|
|
3070
|
+
function installStdioLifecycleGuards() {
|
|
3071
|
+
process.stdin.on("end", () => shutdownFromStdio("stdin end"));
|
|
3072
|
+
process.stdin.on("close", () => shutdownFromStdio("stdin close"));
|
|
3073
|
+
const handleOutputError = (err: any) => {
|
|
3074
|
+
const code = err?.code || err?.name || "output error";
|
|
3075
|
+
if (code === "EPIPE" || code === "ERR_STREAM_DESTROYED") {
|
|
3076
|
+
shutdownFromStdio(String(code));
|
|
3077
|
+
}
|
|
3078
|
+
};
|
|
3079
|
+
process.stdout.on("error", handleOutputError);
|
|
3080
|
+
process.stderr.on("error", handleOutputError);
|
|
3081
|
+
process.on("SIGPIPE", () => shutdownFromStdio("SIGPIPE"));
|
|
3082
|
+
process.on("beforeExit", () => {
|
|
3083
|
+
try { flushLastSeenMessageTs(); } catch {}
|
|
3084
|
+
});
|
|
3085
|
+
}
|
|
3086
|
+
|
|
2941
3087
|
// --- Start ---
|
|
2942
3088
|
async function main() {
|
|
3089
|
+
installStdioLifecycleGuards();
|
|
2943
3090
|
connectWS();
|
|
2944
3091
|
|
|
2945
3092
|
// Stdio is the only supported transport. The --port HTTP SSE path was
|
|
2946
3093
|
// removed in v0.6.7 — OpenClaw users should install the native channel
|
|
2947
3094
|
// adapter `openclaw-agentchat` (npm) instead of running this plugin
|
|
2948
3095
|
// as an HTTP server.
|
|
2949
|
-
|
|
3096
|
+
transport = new StdioServerTransport();
|
|
2950
3097
|
await server.connect(transport);
|
|
2951
3098
|
process.stderr.write("[agentchat] MCP server started (Stdio)\n");
|
|
2952
3099
|
}
|