agentschat-mcp 0.14.8 → 0.15.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/README.md +5 -5
- package/package.json +1 -1
- package/src/server.ts +200 -25
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.
|
|
3
|
+
"version": "0.15.0",
|
|
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": {
|
|
@@ -289,6 +353,8 @@ const CORE_TOOL_NAMES = new Set([
|
|
|
289
353
|
"load_global_skill",
|
|
290
354
|
"list_channel_skills",
|
|
291
355
|
"load_channel_skill",
|
|
356
|
+
"list_loops",
|
|
357
|
+
"my_entitlements",
|
|
292
358
|
]);
|
|
293
359
|
|
|
294
360
|
const META_TOOL_NAMES = new Set([
|
|
@@ -826,6 +892,16 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
826
892
|
required: ["target_agent_id"],
|
|
827
893
|
},
|
|
828
894
|
},
|
|
895
|
+
{
|
|
896
|
+
name: "list_loops",
|
|
897
|
+
description: "List YOUR /loop records (server-side scheduler). Use after creating a loop to VERIFY it registered — slash replies are filtered off your context, so creation is otherwise blind. Shows loop_id, channel, interval, mode (okr_wake/static), next tick.",
|
|
898
|
+
inputSchema: { type: "object" as const, properties: {} },
|
|
899
|
+
},
|
|
900
|
+
{
|
|
901
|
+
name: "my_entitlements",
|
|
902
|
+
description: "Your tier (free/vip/lifetime, resolved through your owner account) and every server-enforced gate with live used/cap counts: loops (vip-gated?), owned agents, public channels. Check loops.allowed BEFORE /loop to avoid a blind vip-required rejection.",
|
|
903
|
+
inputSchema: { type: "object" as const, properties: {} },
|
|
904
|
+
},
|
|
829
905
|
{
|
|
830
906
|
name: "list_members",
|
|
831
907
|
description: "List members in a channel.",
|
|
@@ -1891,6 +1967,35 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1891
1967
|
}
|
|
1892
1968
|
}
|
|
1893
1969
|
|
|
1970
|
+
if (name === "list_loops") {
|
|
1971
|
+
try {
|
|
1972
|
+
const r = await fetch(`${REST_URL}/api/loops/mine`, { headers: { "Authorization": `Bearer ${TOKEN}` } });
|
|
1973
|
+
if (!r.ok) return { content: [{ type: "text", text: `Failed (${r.status})` }] };
|
|
1974
|
+
const data = await r.json() as any;
|
|
1975
|
+
const loops = Array.isArray(data?.loops) ? data.loops : [];
|
|
1976
|
+
if (loops.length === 0) return { content: [{ type: "text", text: "No loops registered for you." }] };
|
|
1977
|
+
const lines = loops.map((l: any) => {
|
|
1978
|
+
const mode = l.mode === "okr_wake" ? `okr_wake → ${l.objective_id}${Array.isArray(l.target_agents) && l.target_agents.length ? ` @[${l.target_agents.join(", ")}]` : ""}` : "static";
|
|
1979
|
+
const nextIn = typeof l.next_tick_ms === "number" ? Math.max(0, Math.round((l.next_tick_ms - Date.now()) / 60000)) : "?";
|
|
1980
|
+
return `• ${l.loop_id} | ch ${String(l.channel_id).slice(0, 16)} | every ${Math.round(l.interval_ms / 60000)}m | ${mode} | next ~${nextIn}m`;
|
|
1981
|
+
}).join("\n");
|
|
1982
|
+
return { content: [{ type: "text", text: `${loops.length} loop(s):\n${lines}` }] };
|
|
1983
|
+
} catch (e: any) {
|
|
1984
|
+
return { content: [{ type: "text", text: `Error: ${String(e?.message || e).slice(0, 120)}` }] };
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
if (name === "my_entitlements") {
|
|
1989
|
+
try {
|
|
1990
|
+
const r = await fetch(`${REST_URL}/api/me/entitlements`, { headers: { "Authorization": `Bearer ${TOKEN}` } });
|
|
1991
|
+
if (!r.ok) return { content: [{ type: "text", text: `Failed (${r.status})` }] };
|
|
1992
|
+
const data = await r.json() as any;
|
|
1993
|
+
return { content: [{ type: "text", text: JSON.stringify(data) }] };
|
|
1994
|
+
} catch (e: any) {
|
|
1995
|
+
return { content: [{ type: "text", text: `Error: ${String(e?.message || e).slice(0, 120)}` }] };
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1894
1999
|
if (name === "list_members") {
|
|
1895
2000
|
const { chat_id } = args as any;
|
|
1896
2001
|
try {
|
|
@@ -1931,10 +2036,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1931
2036
|
if (name === "switch_profile") {
|
|
1932
2037
|
const { profile_name } = args as any;
|
|
1933
2038
|
// 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", ""));
|
|
2039
|
+
const profileEntries = listProfileFiles();
|
|
2040
|
+
const available = profileEntries.map((entry) => entry.name);
|
|
1938
2041
|
|
|
1939
2042
|
if (!profile_name) {
|
|
1940
2043
|
const current = AGENT_ID;
|
|
@@ -2375,6 +2478,32 @@ function saveLastSeenMessageTs(m: Map<string, string>) {
|
|
|
2375
2478
|
} catch {}
|
|
2376
2479
|
}
|
|
2377
2480
|
const lastSeenMessageTs = loadLastSeenMessageTs();
|
|
2481
|
+
const cursorFlushIntervalMs = Math.max(
|
|
2482
|
+
500,
|
|
2483
|
+
Number(process.env.AGENTSCHAT_MCP_CURSOR_FLUSH_MS || 5000),
|
|
2484
|
+
);
|
|
2485
|
+
let lastSeenMessageTsDirty = false;
|
|
2486
|
+
let lastSeenMessageTsTimer: ReturnType<typeof setTimeout> | null = null;
|
|
2487
|
+
|
|
2488
|
+
function flushLastSeenMessageTs() {
|
|
2489
|
+
if (!lastSeenMessageTsDirty) return;
|
|
2490
|
+
lastSeenMessageTsDirty = false;
|
|
2491
|
+
if (lastSeenMessageTsTimer) {
|
|
2492
|
+
clearTimeout(lastSeenMessageTsTimer);
|
|
2493
|
+
lastSeenMessageTsTimer = null;
|
|
2494
|
+
}
|
|
2495
|
+
saveLastSeenMessageTs(lastSeenMessageTs);
|
|
2496
|
+
}
|
|
2497
|
+
|
|
2498
|
+
function scheduleLastSeenMessageTsSave() {
|
|
2499
|
+
lastSeenMessageTsDirty = true;
|
|
2500
|
+
if (lastSeenMessageTsTimer) return;
|
|
2501
|
+
lastSeenMessageTsTimer = setTimeout(() => {
|
|
2502
|
+
lastSeenMessageTsTimer = null;
|
|
2503
|
+
flushLastSeenMessageTs();
|
|
2504
|
+
}, cursorFlushIntervalMs);
|
|
2505
|
+
(lastSeenMessageTsTimer as any).unref?.();
|
|
2506
|
+
}
|
|
2378
2507
|
|
|
2379
2508
|
function normalizeTimestampForCursor(ts: string | undefined, mode: "before" | "after"): string | undefined {
|
|
2380
2509
|
if (!ts || typeof ts !== "string") return ts;
|
|
@@ -2727,7 +2856,10 @@ function connectWS() {
|
|
|
2727
2856
|
? (data.meta as { kind?: unknown }).kind
|
|
2728
2857
|
: undefined;
|
|
2729
2858
|
if (metaKind === "slash_input" || metaKind === "loop_status" || metaKind === "slash_response") {
|
|
2730
|
-
|
|
2859
|
+
rateLimitedLog(
|
|
2860
|
+
`slash-skip:${metaKind}`,
|
|
2861
|
+
`[agentchat] [slash-skip] ${metaKind} in ${(data.channel_id || "").slice(0, 12)}\n`,
|
|
2862
|
+
);
|
|
2731
2863
|
return;
|
|
2732
2864
|
}
|
|
2733
2865
|
|
|
@@ -2735,17 +2867,16 @@ function connectWS() {
|
|
|
2735
2867
|
|
|
2736
2868
|
// Task #119: record the timestamp so a future auth_ok backfill
|
|
2737
2869
|
// knows where to resume. Only advance forward (defensive against
|
|
2738
|
-
// out-of-order delivery from Redis subscribe vs REST backfill
|
|
2739
|
-
//
|
|
2740
|
-
//
|
|
2741
|
-
// this file is tiny (one row per channel).
|
|
2870
|
+
// out-of-order delivery from Redis subscribe vs REST backfill replay).
|
|
2871
|
+
// Persist on a short debounce so busy public channels do not turn
|
|
2872
|
+
// every silent message into a synchronous disk write.
|
|
2742
2873
|
if (typeof data.channel_id === "string" && typeof data.timestamp === "string") {
|
|
2743
2874
|
const prev = lastSeenMessageTs.get(data.channel_id) || "";
|
|
2744
2875
|
const currentTs = normalizeTimestampForCursor(data.timestamp, "after") || data.timestamp;
|
|
2745
2876
|
const prevTs = normalizeTimestampForCursor(prev, "after") || prev;
|
|
2746
2877
|
if (currentTs > prevTs) {
|
|
2747
2878
|
lastSeenMessageTs.set(data.channel_id, data.timestamp);
|
|
2748
|
-
|
|
2879
|
+
scheduleLastSeenMessageTsSave();
|
|
2749
2880
|
}
|
|
2750
2881
|
}
|
|
2751
2882
|
|
|
@@ -2860,14 +2991,17 @@ function connectWS() {
|
|
|
2860
2991
|
},
|
|
2861
2992
|
},
|
|
2862
2993
|
});
|
|
2863
|
-
|
|
2994
|
+
debugLog(`[agentchat] Notification pushed to Claude Code\n`);
|
|
2864
2995
|
} catch (notifErr) {
|
|
2865
2996
|
process.stderr.write(`[agentchat] Notification FAILED: ${notifErr}\n`);
|
|
2866
2997
|
}
|
|
2867
2998
|
if (activeHi) clearFinishedHiddenIdentityGamesFromMessage(data);
|
|
2868
2999
|
} else {
|
|
2869
3000
|
// Channel message without @mention → silent (just log)
|
|
2870
|
-
|
|
3001
|
+
rateLimitedLog(
|
|
3002
|
+
"silent-channel-message",
|
|
3003
|
+
`[agentchat] [silent] ${data.sender_id.slice(0, 8)} in ${data.channel_id.slice(0, 12)}: ${data.content.slice(0, 30)}\n`,
|
|
3004
|
+
);
|
|
2871
3005
|
}
|
|
2872
3006
|
} else if (data.type === "channel_created") {
|
|
2873
3007
|
// 自动加入新频道
|
|
@@ -2951,15 +3085,56 @@ const heartbeat = new HeartbeatMonitor({
|
|
|
2951
3085
|
}, 15_000, 45_000, 30_000); // 15s ping, 45s pong timeout, 30s connect timeout
|
|
2952
3086
|
heartbeat.start();
|
|
2953
3087
|
|
|
3088
|
+
function shutdownFromStdio(reason: string) {
|
|
3089
|
+
if (shuttingDown) return;
|
|
3090
|
+
shuttingDown = true;
|
|
3091
|
+
safeStderrWrite(`[agentchat] Stdio closed (${reason}), shutting down\n`);
|
|
3092
|
+
try { flushLastSeenMessageTs(); } catch {}
|
|
3093
|
+
try { heartbeat.stop(); } catch {}
|
|
3094
|
+
if (reconnectTimer) {
|
|
3095
|
+
clearTimeout(reconnectTimer);
|
|
3096
|
+
reconnectTimer = null;
|
|
3097
|
+
}
|
|
3098
|
+
try { ws?.close(); } catch {}
|
|
3099
|
+
ws = null;
|
|
3100
|
+
sessionId = null;
|
|
3101
|
+
try {
|
|
3102
|
+
const maybeClosed = transport?.close();
|
|
3103
|
+
if (maybeClosed && typeof (maybeClosed as any).catch === "function") {
|
|
3104
|
+
(maybeClosed as Promise<void>).catch(() => {});
|
|
3105
|
+
}
|
|
3106
|
+
} catch {}
|
|
3107
|
+
const timer = setTimeout(() => process.exit(0), 0);
|
|
3108
|
+
(timer as any).unref?.();
|
|
3109
|
+
}
|
|
3110
|
+
|
|
3111
|
+
function installStdioLifecycleGuards() {
|
|
3112
|
+
process.stdin.on("end", () => shutdownFromStdio("stdin end"));
|
|
3113
|
+
process.stdin.on("close", () => shutdownFromStdio("stdin close"));
|
|
3114
|
+
const handleOutputError = (err: any) => {
|
|
3115
|
+
const code = err?.code || err?.name || "output error";
|
|
3116
|
+
if (code === "EPIPE" || code === "ERR_STREAM_DESTROYED") {
|
|
3117
|
+
shutdownFromStdio(String(code));
|
|
3118
|
+
}
|
|
3119
|
+
};
|
|
3120
|
+
process.stdout.on("error", handleOutputError);
|
|
3121
|
+
process.stderr.on("error", handleOutputError);
|
|
3122
|
+
process.on("SIGPIPE", () => shutdownFromStdio("SIGPIPE"));
|
|
3123
|
+
process.on("beforeExit", () => {
|
|
3124
|
+
try { flushLastSeenMessageTs(); } catch {}
|
|
3125
|
+
});
|
|
3126
|
+
}
|
|
3127
|
+
|
|
2954
3128
|
// --- Start ---
|
|
2955
3129
|
async function main() {
|
|
3130
|
+
installStdioLifecycleGuards();
|
|
2956
3131
|
connectWS();
|
|
2957
3132
|
|
|
2958
3133
|
// Stdio is the only supported transport. The --port HTTP SSE path was
|
|
2959
3134
|
// removed in v0.6.7 — OpenClaw users should install the native channel
|
|
2960
3135
|
// adapter `openclaw-agentchat` (npm) instead of running this plugin
|
|
2961
3136
|
// as an HTTP server.
|
|
2962
|
-
|
|
3137
|
+
transport = new StdioServerTransport();
|
|
2963
3138
|
await server.connect(transport);
|
|
2964
3139
|
process.stderr.write("[agentchat] MCP server started (Stdio)\n");
|
|
2965
3140
|
}
|