@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tjamescouch/agentchat-mcp",
3
- "version": "0.8.0",
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.21.1",
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 = 'wss://agentchat-server.fly.dev';
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 { client, getLastSeen, updateLastSeen } from '../state.js';
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
- // Join channels
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
- const startTime = Date.now();
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
- // Check daemon inbox for missed messages first
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: true,
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);