@tjamescouch/agentchat-mcp 0.8.0 → 0.8.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/package.json +2 -2
- package/state.js +44 -1
- package/tools/connect.js +18 -3
- package/tools/listen.js +55 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tjamescouch/agentchat-mcp",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.2",
|
|
4
4
|
"description": "MCP server for AgentChat - real-time AI agent communication",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"license": "MIT",
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
30
|
-
"@tjamescouch/agentchat": "^0.
|
|
30
|
+
"@tjamescouch/agentchat": "^0.22.1",
|
|
31
31
|
"zod": "^3.25.0"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
package/state.js
CHANGED
|
@@ -12,8 +12,25 @@ export let keepaliveInterval = null;
|
|
|
12
12
|
// Message tracking - timestamp of last message we returned to the caller
|
|
13
13
|
export let lastSeenTimestamp = 0;
|
|
14
14
|
|
|
15
|
+
// Message buffer - captures messages between listen() calls
|
|
16
|
+
const MAX_BUFFER_SIZE = 200;
|
|
17
|
+
let messageBuffer = [];
|
|
18
|
+
|
|
15
19
|
// Default server
|
|
16
|
-
export const DEFAULT_SERVER_URL =
|
|
20
|
+
export const DEFAULT_SERVER_URL = (() => {
|
|
21
|
+
const explicit = process.env.AGENTCHAT_URL;
|
|
22
|
+
if (explicit) {
|
|
23
|
+
const parsed = new URL(explicit);
|
|
24
|
+
const isLocal = parsed.hostname === 'localhost' || parsed.hostname === '127.0.0.1' || parsed.hostname === '::1';
|
|
25
|
+
if (!isLocal && process.env.AGENTCHAT_PUBLIC !== 'true') {
|
|
26
|
+
console.error(`ERROR: AGENTCHAT_URL points to remote host "${parsed.hostname}" but AGENTCHAT_PUBLIC is not set.`);
|
|
27
|
+
console.error('Set AGENTCHAT_PUBLIC=true to allow connections to non-localhost servers.');
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
return explicit;
|
|
31
|
+
}
|
|
32
|
+
return process.env.AGENTCHAT_PUBLIC === 'true' ? 'wss://agentchat-server.fly.dev' : 'ws://localhost:6667';
|
|
33
|
+
})();
|
|
17
34
|
|
|
18
35
|
// Keepalive settings
|
|
19
36
|
export const KEEPALIVE_INTERVAL_MS = 30000;
|
|
@@ -69,3 +86,29 @@ export function resetLastSeen() {
|
|
|
69
86
|
export function getLastSeen() {
|
|
70
87
|
return lastSeenTimestamp;
|
|
71
88
|
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Push a message into the buffer (called by persistent handler on client)
|
|
92
|
+
*/
|
|
93
|
+
export function bufferMessage(msg) {
|
|
94
|
+
messageBuffer.push(msg);
|
|
95
|
+
if (messageBuffer.length > MAX_BUFFER_SIZE) {
|
|
96
|
+
messageBuffer = messageBuffer.slice(-MAX_BUFFER_SIZE);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Drain all buffered messages and clear the buffer
|
|
102
|
+
*/
|
|
103
|
+
export function drainMessageBuffer() {
|
|
104
|
+
const messages = messageBuffer;
|
|
105
|
+
messageBuffer = [];
|
|
106
|
+
return messages;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Clear the message buffer (e.g., on reconnect)
|
|
111
|
+
*/
|
|
112
|
+
export function clearMessageBuffer() {
|
|
113
|
+
messageBuffer = [];
|
|
114
|
+
}
|
package/tools/connect.js
CHANGED
|
@@ -10,7 +10,7 @@ import path from 'path';
|
|
|
10
10
|
import {
|
|
11
11
|
client, keepaliveInterval,
|
|
12
12
|
setClient, setServerUrl, setKeepaliveInterval,
|
|
13
|
-
resetLastSeen,
|
|
13
|
+
resetLastSeen, clearMessageBuffer, bufferMessage,
|
|
14
14
|
DEFAULT_SERVER_URL, KEEPALIVE_INTERVAL_MS
|
|
15
15
|
} from '../state.js';
|
|
16
16
|
|
|
@@ -55,7 +55,7 @@ export function registerConnectTool(server) {
|
|
|
55
55
|
'agentchat_connect',
|
|
56
56
|
'Connect to an AgentChat server for real-time agent communication',
|
|
57
57
|
{
|
|
58
|
-
server_url: z.string().optional().describe('WebSocket URL (default: wss://agentchat-server.fly.dev)'),
|
|
58
|
+
server_url: z.string().optional().describe('WebSocket URL (default: ws://localhost:6667, or wss://agentchat-server.fly.dev if AGENTCHAT_PUBLIC=true)'),
|
|
59
59
|
name: z.string().optional().describe('Agent name for persistent identity. Creates .agentchat/identities/<name>.json. Omit for ephemeral identity.'),
|
|
60
60
|
identity_path: z.string().optional().describe('Custom path to identity file (overrides name)'),
|
|
61
61
|
},
|
|
@@ -132,8 +132,23 @@ export function registerConnectTool(server) {
|
|
|
132
132
|
setClient(newClient);
|
|
133
133
|
setServerUrl(actualServerUrl);
|
|
134
134
|
|
|
135
|
-
// Reset last seen timestamp on new connection
|
|
135
|
+
// Reset last seen timestamp and message buffer on new connection
|
|
136
136
|
resetLastSeen();
|
|
137
|
+
clearMessageBuffer();
|
|
138
|
+
|
|
139
|
+
// Persistent message handler - buffers ALL messages between listen() calls
|
|
140
|
+
// This is the fix for the listen() message drop bug: messages that arrive
|
|
141
|
+
// while the agent is busy with other tools are captured here instead of lost.
|
|
142
|
+
newClient.on('message', (msg) => {
|
|
143
|
+
// Skip own messages, server noise, and replays
|
|
144
|
+
if (msg.from === newClient.agentId || msg.from === '@server' || msg.replay) return;
|
|
145
|
+
bufferMessage({
|
|
146
|
+
from: msg.from,
|
|
147
|
+
to: msg.to,
|
|
148
|
+
content: msg.content,
|
|
149
|
+
ts: msg.ts,
|
|
150
|
+
});
|
|
151
|
+
});
|
|
137
152
|
|
|
138
153
|
// Start keepalive ping to prevent connection timeout
|
|
139
154
|
const interval = setInterval(() => {
|
package/tools/listen.js
CHANGED
|
@@ -7,7 +7,8 @@ import { z } from 'zod';
|
|
|
7
7
|
import fs from 'fs';
|
|
8
8
|
import { getDaemonPaths } from '@tjamescouch/agentchat/lib/daemon.js';
|
|
9
9
|
import { addJitter } from '@tjamescouch/agentchat/lib/jitter.js';
|
|
10
|
-
import {
|
|
10
|
+
import { ClientMessageType } from '@tjamescouch/agentchat/lib/protocol.js';
|
|
11
|
+
import { client, getLastSeen, updateLastSeen, drainMessageBuffer } from '../state.js';
|
|
11
12
|
|
|
12
13
|
// Timeouts - agent cannot override these
|
|
13
14
|
const ENFORCED_TIMEOUT_MS = 60 * 60 * 1000; // 1 hour when alone
|
|
@@ -32,14 +33,33 @@ export function registerListenTool(server) {
|
|
|
32
33
|
};
|
|
33
34
|
}
|
|
34
35
|
|
|
35
|
-
|
|
36
|
+
const startTime = Date.now();
|
|
37
|
+
|
|
38
|
+
// Collect replay messages during channel joins
|
|
39
|
+
// The server sends buffered messages with { replay: true } on join,
|
|
40
|
+
// so we must capture them before they're emitted and lost
|
|
41
|
+
const replayMessages = [];
|
|
42
|
+
const replayHandler = (msg) => {
|
|
43
|
+
if (msg.replay && msg.from !== client.agentId && msg.from !== '@server') {
|
|
44
|
+
replayMessages.push({
|
|
45
|
+
from: msg.from,
|
|
46
|
+
to: msg.to,
|
|
47
|
+
content: msg.content,
|
|
48
|
+
ts: msg.ts,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
client.on('message', replayHandler);
|
|
53
|
+
|
|
54
|
+
// Join channels (replay messages arrive here)
|
|
36
55
|
for (const channel of channels) {
|
|
37
56
|
if (!client.channels.has(channel)) {
|
|
38
57
|
await client.join(channel);
|
|
39
58
|
}
|
|
40
59
|
}
|
|
41
60
|
|
|
42
|
-
|
|
61
|
+
// Done collecting replays
|
|
62
|
+
client.removeListener('message', replayHandler);
|
|
43
63
|
|
|
44
64
|
// Check channel occupancy to determine timeout behavior
|
|
45
65
|
let othersPresent = false;
|
|
@@ -61,9 +81,22 @@ export function registerListenTool(server) {
|
|
|
61
81
|
}
|
|
62
82
|
const lastSeen = getLastSeen();
|
|
63
83
|
|
|
64
|
-
//
|
|
84
|
+
// Set presence to 'listening' so other agents see we're active
|
|
85
|
+
const setPresence = (status) => {
|
|
86
|
+
client.sendRaw({ type: ClientMessageType.SET_PRESENCE, status });
|
|
87
|
+
};
|
|
88
|
+
setPresence('listening');
|
|
89
|
+
|
|
90
|
+
// Drain the persistent message buffer (messages captured between listen calls)
|
|
91
|
+
const buffered = drainMessageBuffer().filter(m => {
|
|
92
|
+
// Filter to relevant channels/DMs, skip already-seen messages
|
|
93
|
+
const isRelevant = channels.includes(m.to) || m.to === client.agentId;
|
|
94
|
+
return isRelevant && (!m.ts || m.ts > lastSeen);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Start with buffered messages + replay messages captured during join
|
|
65
98
|
const paths = getDaemonPaths('default');
|
|
66
|
-
let missedMessages = [];
|
|
99
|
+
let missedMessages = [...buffered, ...replayMessages];
|
|
67
100
|
|
|
68
101
|
if (fs.existsSync(paths.inbox)) {
|
|
69
102
|
try {
|
|
@@ -106,19 +139,33 @@ export function registerListenTool(server) {
|
|
|
106
139
|
}
|
|
107
140
|
}
|
|
108
141
|
|
|
109
|
-
// If we have missed messages, return them immediately
|
|
142
|
+
// If we have missed messages (from replay or inbox), return them immediately
|
|
110
143
|
if (missedMessages.length > 0) {
|
|
144
|
+
// Deduplicate by timestamp + from (replay and inbox may overlap)
|
|
145
|
+
const seen = new Set();
|
|
146
|
+
missedMessages = missedMessages.filter((m) => {
|
|
147
|
+
const key = `${m.ts}:${m.from}`;
|
|
148
|
+
if (seen.has(key)) return false;
|
|
149
|
+
seen.add(key);
|
|
150
|
+
return true;
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// Sort by timestamp ascending (oldest first)
|
|
154
|
+
missedMessages.sort((a, b) => a.ts - b.ts);
|
|
155
|
+
|
|
111
156
|
// Update last seen to the newest message timestamp
|
|
112
157
|
const newestTs = missedMessages[missedMessages.length - 1].ts;
|
|
113
158
|
updateLastSeen(newestTs);
|
|
114
159
|
|
|
160
|
+
setPresence('online');
|
|
115
161
|
return {
|
|
116
162
|
content: [
|
|
117
163
|
{
|
|
118
164
|
type: 'text',
|
|
119
165
|
text: JSON.stringify({
|
|
120
166
|
messages: missedMessages,
|
|
121
|
-
from_inbox:
|
|
167
|
+
from_inbox: replayMessages.length === 0,
|
|
168
|
+
from_replay: replayMessages.length > 0,
|
|
122
169
|
elapsed_ms: Date.now() - startTime,
|
|
123
170
|
}),
|
|
124
171
|
},
|
|
@@ -165,6 +212,7 @@ export function registerListenTool(server) {
|
|
|
165
212
|
const cleanup = () => {
|
|
166
213
|
client.removeListener('message', messageHandler);
|
|
167
214
|
if (timeoutId) clearTimeout(timeoutId);
|
|
215
|
+
setPresence('online');
|
|
168
216
|
};
|
|
169
217
|
|
|
170
218
|
client.on('message', messageHandler);
|