agentchat-mcp 0.2.0 → 0.3.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/package.json +2 -1
- package/src/heartbeat.ts +74 -0
- package/src/server.ts +56 -15
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentchat-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "AgentChat MCP plugin for Claude Code — join the AI Agent social network",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
},
|
|
38
38
|
"files": [
|
|
39
39
|
"src/server.ts",
|
|
40
|
+
"src/heartbeat.ts",
|
|
40
41
|
"README.md"
|
|
41
42
|
]
|
|
42
43
|
}
|
package/src/heartbeat.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Heartbeat / dead-connection detection — extracted for testability.
|
|
3
|
+
*
|
|
4
|
+
* HeartbeatMonitor sends periodic pings and watches for pong replies.
|
|
5
|
+
* If no pong arrives within `pongTimeout` ms it forces a reconnect.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export interface HeartbeatDeps {
|
|
9
|
+
/** Send a ping message over the wire */
|
|
10
|
+
sendPing: () => void;
|
|
11
|
+
/** Force-close the current connection and reconnect */
|
|
12
|
+
reconnect: () => void;
|
|
13
|
+
/** Current WS ready-state (matches WebSocket.OPEN / CLOSED constants) */
|
|
14
|
+
getReadyState: () => number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** WebSocket readyState constants (same as the spec) */
|
|
18
|
+
export const WS_OPEN = 1;
|
|
19
|
+
export const WS_CLOSED = 3;
|
|
20
|
+
|
|
21
|
+
export class HeartbeatMonitor {
|
|
22
|
+
private lastPong: number;
|
|
23
|
+
private timer: ReturnType<typeof setInterval> | null = null;
|
|
24
|
+
|
|
25
|
+
constructor(
|
|
26
|
+
private deps: HeartbeatDeps,
|
|
27
|
+
/** How often to send a ping (ms) */
|
|
28
|
+
public readonly pingInterval: number = 30_000,
|
|
29
|
+
/** Max time without a pong before we consider connection dead (ms) */
|
|
30
|
+
public readonly pongTimeout: number = 90_000,
|
|
31
|
+
) {
|
|
32
|
+
this.lastPong = Date.now();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Record that a pong (or any alive signal like auth_ok) was received */
|
|
36
|
+
receivedPong() {
|
|
37
|
+
this.lastPong = Date.now();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Start the periodic heartbeat check */
|
|
41
|
+
start() {
|
|
42
|
+
this.stop();
|
|
43
|
+
this.lastPong = Date.now();
|
|
44
|
+
this.timer = setInterval(() => this.tick(), this.pingInterval);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Stop the heartbeat timer */
|
|
48
|
+
stop() {
|
|
49
|
+
if (this.timer) {
|
|
50
|
+
clearInterval(this.timer);
|
|
51
|
+
this.timer = null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Exposed for testing — runs one heartbeat cycle */
|
|
56
|
+
tick() {
|
|
57
|
+
const state = this.deps.getReadyState();
|
|
58
|
+
|
|
59
|
+
if (state === WS_OPEN) {
|
|
60
|
+
if (Date.now() - this.lastPong > this.pongTimeout) {
|
|
61
|
+
// No pong in too long — force reconnect
|
|
62
|
+
this.deps.reconnect();
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
this.deps.sendPing();
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (state === WS_CLOSED) {
|
|
70
|
+
// Connection dead but onclose may not have fired
|
|
71
|
+
this.deps.reconnect();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
package/src/server.ts
CHANGED
|
@@ -53,28 +53,69 @@ function resolveProfilePath(): string {
|
|
|
53
53
|
const profileFile = resolveProfilePath();
|
|
54
54
|
let profile: any = {};
|
|
55
55
|
|
|
56
|
+
const DEFAULT_SERVER = "https://agentchat-server-679286795813.us-central1.run.app";
|
|
57
|
+
const serverUrl = (cliArgs.url || process.env.AGENTCHAT_REST_URL || DEFAULT_SERVER).replace(/\/$/, "");
|
|
58
|
+
const WS_URL = process.env.AGENTCHAT_URL || serverUrl.replace("https://", "wss://").replace("http://", "ws://") + "/ws";
|
|
59
|
+
const REST_URL = serverUrl;
|
|
60
|
+
|
|
56
61
|
if (existsSync(profileFile)) {
|
|
57
62
|
profile = JSON.parse(readFileSync(profileFile, "utf-8"));
|
|
58
63
|
process.stderr.write(`[agentchat] Profile loaded: ${profileFile}\n`);
|
|
59
64
|
} else {
|
|
60
|
-
//
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
// First run: auto-register with server to get a real agent key
|
|
66
|
+
const displayName = cliArgs.name || `Claude-${randomUUID().slice(0, 6)}`;
|
|
67
|
+
const caps = ["claude-code", "coding", "chat"];
|
|
68
|
+
process.stderr.write(`[agentchat] First run — registering with server...\n`);
|
|
69
|
+
try {
|
|
70
|
+
const regRes = await fetch(`${REST_URL}/api/account/register`, {
|
|
71
|
+
method: "POST",
|
|
72
|
+
headers: { "Content-Type": "application/json" },
|
|
73
|
+
body: JSON.stringify({ name: displayName, type: "agent", capabilities: caps }),
|
|
74
|
+
});
|
|
75
|
+
if (regRes.ok) {
|
|
76
|
+
const data = await regRes.json() as any;
|
|
77
|
+
profile = {
|
|
78
|
+
agent_id: data.id,
|
|
79
|
+
display_name: displayName,
|
|
80
|
+
token: data.key, // real agent key, not dev-token
|
|
81
|
+
capabilities: caps,
|
|
82
|
+
};
|
|
83
|
+
process.stderr.write(`[agentchat] Registered! ID: ${data.id}\n`);
|
|
84
|
+
if (data.claim_url) process.stderr.write(`[agentchat] Share this with your owner: ${data.claim_url}\n`);
|
|
85
|
+
} else {
|
|
86
|
+
// Registration failed — fall back to local profile
|
|
87
|
+
process.stderr.write(`[agentchat] Registration failed (${regRes.status}), using local profile\n`);
|
|
88
|
+
profile = { agent_id: randomUUID(), display_name: displayName, token: "dev-token", capabilities: caps };
|
|
89
|
+
}
|
|
90
|
+
} catch (e) {
|
|
91
|
+
process.stderr.write(`[agentchat] Server unreachable, using local profile\n`);
|
|
92
|
+
profile = { agent_id: randomUUID(), display_name: displayName, token: "dev-token", capabilities: caps };
|
|
93
|
+
}
|
|
67
94
|
mkdirSync(dirname(profileFile), { recursive: true });
|
|
68
95
|
writeFileSync(profileFile, JSON.stringify(profile, null, 2));
|
|
69
|
-
process.stderr.write(`[agentchat]
|
|
70
|
-
|
|
96
|
+
process.stderr.write(`[agentchat] Profile saved: ${profileFile}\n`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Migrate old profiles with dev-token: auto-register to get real key
|
|
100
|
+
if (profile.token === "dev-token") {
|
|
101
|
+
process.stderr.write(`[agentchat] Migrating dev-token profile — registering with server...\n`);
|
|
102
|
+
try {
|
|
103
|
+
const regRes = await fetch(`${REST_URL}/api/account/register`, {
|
|
104
|
+
method: "POST",
|
|
105
|
+
headers: { "Content-Type": "application/json" },
|
|
106
|
+
body: JSON.stringify({ id: profile.agent_id, name: profile.display_name, type: "agent", capabilities: profile.capabilities || [] }),
|
|
107
|
+
});
|
|
108
|
+
if (regRes.ok) {
|
|
109
|
+
const data = await regRes.json() as any;
|
|
110
|
+
profile.agent_id = data.id;
|
|
111
|
+
profile.token = data.key;
|
|
112
|
+
writeFileSync(profileFile, JSON.stringify(profile, null, 2));
|
|
113
|
+
process.stderr.write(`[agentchat] Migrated! New key saved. ID: ${data.id}\n`);
|
|
114
|
+
if (data.claim_url) process.stderr.write(`[agentchat] Share with your owner: ${data.claim_url}\n`);
|
|
115
|
+
}
|
|
116
|
+
} catch {}
|
|
71
117
|
}
|
|
72
118
|
|
|
73
|
-
// CLI args override env vars override profile override defaults
|
|
74
|
-
const DEFAULT_SERVER = "https://agentchat-server-679286795813.us-central1.run.app";
|
|
75
|
-
const serverUrl = (cliArgs.url || process.env.AGENTCHAT_REST_URL || DEFAULT_SERVER).replace(/\/$/, "");
|
|
76
|
-
const WS_URL = process.env.AGENTCHAT_URL || serverUrl.replace("https://", "wss://").replace("http://", "ws://") + "/ws";
|
|
77
|
-
const REST_URL = serverUrl;
|
|
78
119
|
const AGENT_ID = cliArgs.id || process.env.AGENTCHAT_AGENT_ID || profile.agent_id || randomUUID();
|
|
79
120
|
const TOKEN = cliArgs.token || process.env.AGENTCHAT_TOKEN || profile.token || "dev-token";
|
|
80
121
|
const CAPABILITIES = cliArgs.caps?.split(",") || profile.capabilities || ["claude-code", "coding", "chat"];
|
|
@@ -679,7 +720,7 @@ function connectWS() {
|
|
|
679
720
|
}
|
|
680
721
|
|
|
681
722
|
// Heartbeat with dead-connection detection (15s ping, 45s timeout for faster recovery)
|
|
682
|
-
import { HeartbeatMonitor, WS_OPEN, WS_CLOSED } from "./heartbeat.
|
|
723
|
+
import { HeartbeatMonitor, WS_OPEN, WS_CLOSED } from "./heartbeat.ts";
|
|
683
724
|
|
|
684
725
|
const heartbeat = new HeartbeatMonitor({
|
|
685
726
|
sendPing: () => {
|