@tjamescouch/agentchat-mcp 0.5.0 → 0.6.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/index.js +4 -446
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -7,31 +7,8 @@
|
|
|
7
7
|
|
|
8
8
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
9
9
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import { AgentChatDaemon, getDaemonPaths, isDaemonRunning, stopDaemon } from '@tjamescouch/agentchat/lib/daemon.js';
|
|
13
|
-
import { addJitter } from '@tjamescouch/agentchat/lib/jitter.js';
|
|
14
|
-
import fs from 'fs';
|
|
15
|
-
import path from 'path';
|
|
16
|
-
|
|
17
|
-
// Global state
|
|
18
|
-
let client = null;
|
|
19
|
-
let daemon = null;
|
|
20
|
-
let serverUrl = null;
|
|
21
|
-
let keepaliveInterval = null;
|
|
22
|
-
|
|
23
|
-
// Keepalive settings
|
|
24
|
-
const KEEPALIVE_INTERVAL_MS = 30000; // Ping every 30 seconds
|
|
25
|
-
|
|
26
|
-
// Default server
|
|
27
|
-
const DEFAULT_SERVER_URL = 'wss://agentchat-server.fly.dev';
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Get identity path for a named agent
|
|
31
|
-
*/
|
|
32
|
-
function getIdentityPath(name) {
|
|
33
|
-
return path.join(process.cwd(), '.agentchat', 'identities', `${name}.json`);
|
|
34
|
-
}
|
|
10
|
+
import { registerAllTools } from './tools/index.js';
|
|
11
|
+
import { client, daemon, keepaliveInterval } from './state.js';
|
|
35
12
|
|
|
36
13
|
/**
|
|
37
14
|
* Create and configure the MCP server
|
|
@@ -42,427 +19,8 @@ function createServer() {
|
|
|
42
19
|
version: '0.1.0',
|
|
43
20
|
});
|
|
44
21
|
|
|
45
|
-
//
|
|
46
|
-
server
|
|
47
|
-
'agentchat_connect',
|
|
48
|
-
'Connect to an AgentChat server for real-time agent communication',
|
|
49
|
-
{
|
|
50
|
-
server_url: z.string().optional().describe('WebSocket URL (default: wss://agentchat-server.fly.dev)'),
|
|
51
|
-
name: z.string().optional().describe('Agent name for persistent identity. Creates .agentchat/identities/<name>.json. Omit for ephemeral identity.'),
|
|
52
|
-
identity_path: z.string().optional().describe('Custom path to identity file (overrides name)'),
|
|
53
|
-
},
|
|
54
|
-
async ({ server_url, name, identity_path }) => {
|
|
55
|
-
try {
|
|
56
|
-
// Stop existing keepalive
|
|
57
|
-
if (keepaliveInterval) {
|
|
58
|
-
clearInterval(keepaliveInterval);
|
|
59
|
-
keepaliveInterval = null;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// Disconnect existing client
|
|
63
|
-
if (client) {
|
|
64
|
-
client.disconnect();
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const actualServerUrl = server_url || DEFAULT_SERVER_URL;
|
|
68
|
-
|
|
69
|
-
// Determine identity path: explicit path > named > ephemeral (none)
|
|
70
|
-
let actualIdentityPath = null;
|
|
71
|
-
if (identity_path) {
|
|
72
|
-
actualIdentityPath = identity_path;
|
|
73
|
-
} else if (name) {
|
|
74
|
-
actualIdentityPath = getIdentityPath(name);
|
|
75
|
-
}
|
|
76
|
-
// If neither provided, identity stays null = ephemeral
|
|
77
|
-
|
|
78
|
-
const options = {
|
|
79
|
-
server: actualServerUrl,
|
|
80
|
-
name: name || `mcp-agent-${process.pid}`,
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
// Set up persistent identity if path specified
|
|
84
|
-
if (actualIdentityPath) {
|
|
85
|
-
const identityDir = path.dirname(actualIdentityPath);
|
|
86
|
-
if (!fs.existsSync(identityDir)) {
|
|
87
|
-
fs.mkdirSync(identityDir, { recursive: true });
|
|
88
|
-
}
|
|
89
|
-
// Use identity if it exists, otherwise client will create one
|
|
90
|
-
if (fs.existsSync(actualIdentityPath)) {
|
|
91
|
-
options.identity = actualIdentityPath;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
client = new AgentChatClient(options);
|
|
96
|
-
await client.connect();
|
|
97
|
-
serverUrl = actualServerUrl;
|
|
98
|
-
|
|
99
|
-
// Start keepalive ping to prevent connection timeout
|
|
100
|
-
keepaliveInterval = setInterval(() => {
|
|
101
|
-
try {
|
|
102
|
-
if (client && client.connected) {
|
|
103
|
-
client.ping();
|
|
104
|
-
}
|
|
105
|
-
} catch (e) {
|
|
106
|
-
// Connection likely dead, will reconnect on next tool call
|
|
107
|
-
}
|
|
108
|
-
}, KEEPALIVE_INTERVAL_MS);
|
|
109
|
-
|
|
110
|
-
return {
|
|
111
|
-
content: [
|
|
112
|
-
{
|
|
113
|
-
type: 'text',
|
|
114
|
-
text: JSON.stringify({
|
|
115
|
-
success: true,
|
|
116
|
-
agent_id: client.agentId,
|
|
117
|
-
server: actualServerUrl,
|
|
118
|
-
persistent: !!actualIdentityPath,
|
|
119
|
-
identity_path: actualIdentityPath,
|
|
120
|
-
}),
|
|
121
|
-
},
|
|
122
|
-
],
|
|
123
|
-
};
|
|
124
|
-
} catch (error) {
|
|
125
|
-
return {
|
|
126
|
-
content: [{ type: 'text', text: `Error connecting: ${error.message}` }],
|
|
127
|
-
isError: true,
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
);
|
|
132
|
-
|
|
133
|
-
// Tool: Send message
|
|
134
|
-
server.tool(
|
|
135
|
-
'agentchat_send',
|
|
136
|
-
'Send a message to a channel (#channel) or agent (@agent)',
|
|
137
|
-
{
|
|
138
|
-
target: z.string().describe('Target: #channel or @agent-id'),
|
|
139
|
-
message: z.string().describe('Message content to send'),
|
|
140
|
-
},
|
|
141
|
-
async ({ target, message }) => {
|
|
142
|
-
try {
|
|
143
|
-
if (!client || !client.connected) {
|
|
144
|
-
return {
|
|
145
|
-
content: [{ type: 'text', text: 'Not connected. Use agentchat_connect first.' }],
|
|
146
|
-
isError: true,
|
|
147
|
-
};
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// Join channel if needed
|
|
151
|
-
if (target.startsWith('#') && !client.channels.has(target)) {
|
|
152
|
-
await client.join(target);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
await client.send(target, message);
|
|
156
|
-
|
|
157
|
-
return {
|
|
158
|
-
content: [
|
|
159
|
-
{
|
|
160
|
-
type: 'text',
|
|
161
|
-
text: JSON.stringify({
|
|
162
|
-
success: true,
|
|
163
|
-
target,
|
|
164
|
-
message,
|
|
165
|
-
from: client.agentId,
|
|
166
|
-
}),
|
|
167
|
-
},
|
|
168
|
-
],
|
|
169
|
-
};
|
|
170
|
-
} catch (error) {
|
|
171
|
-
return {
|
|
172
|
-
content: [{ type: 'text', text: `Error sending: ${error.message}` }],
|
|
173
|
-
isError: true,
|
|
174
|
-
};
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
);
|
|
178
|
-
|
|
179
|
-
// Tool: Listen for messages (returns immediately when a message arrives)
|
|
180
|
-
server.tool(
|
|
181
|
-
'agentchat_listen',
|
|
182
|
-
'Listen for messages - blocks until a message arrives. No timeout by default (waits forever).',
|
|
183
|
-
{
|
|
184
|
-
channels: z.array(z.string()).describe('Channels to listen on (e.g., ["#general"])'),
|
|
185
|
-
timeout_ms: z.number().optional().describe('Optional timeout in milliseconds. Omit to wait forever.'),
|
|
186
|
-
},
|
|
187
|
-
async ({ channels, timeout_ms }) => {
|
|
188
|
-
try {
|
|
189
|
-
if (!client || !client.connected) {
|
|
190
|
-
return {
|
|
191
|
-
content: [{ type: 'text', text: 'Not connected. Use agentchat_connect first.' }],
|
|
192
|
-
isError: true,
|
|
193
|
-
};
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
// Join channels
|
|
197
|
-
for (const channel of channels) {
|
|
198
|
-
if (!client.channels.has(channel)) {
|
|
199
|
-
await client.join(channel);
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
const startTime = Date.now();
|
|
204
|
-
|
|
205
|
-
return new Promise((resolve) => {
|
|
206
|
-
let timeoutId = null;
|
|
207
|
-
|
|
208
|
-
const messageHandler = (msg) => {
|
|
209
|
-
// Filter out own messages, replays, and server messages
|
|
210
|
-
if (msg.from === client.agentId || msg.replay || msg.from === '@server') {
|
|
211
|
-
return;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// Got a real message - return immediately
|
|
215
|
-
cleanup();
|
|
216
|
-
resolve({
|
|
217
|
-
content: [
|
|
218
|
-
{
|
|
219
|
-
type: 'text',
|
|
220
|
-
text: JSON.stringify({
|
|
221
|
-
message: {
|
|
222
|
-
from: msg.from,
|
|
223
|
-
to: msg.to,
|
|
224
|
-
content: msg.content,
|
|
225
|
-
ts: msg.ts,
|
|
226
|
-
},
|
|
227
|
-
elapsed_ms: Date.now() - startTime,
|
|
228
|
-
}),
|
|
229
|
-
},
|
|
230
|
-
],
|
|
231
|
-
});
|
|
232
|
-
};
|
|
233
|
-
|
|
234
|
-
const cleanup = () => {
|
|
235
|
-
client.removeListener('message', messageHandler);
|
|
236
|
-
if (timeoutId) clearTimeout(timeoutId);
|
|
237
|
-
};
|
|
238
|
-
|
|
239
|
-
client.on('message', messageHandler);
|
|
240
|
-
|
|
241
|
-
// Only set timeout if specified
|
|
242
|
-
if (timeout_ms) {
|
|
243
|
-
const actualTimeout = addJitter(timeout_ms, 0.2);
|
|
244
|
-
timeoutId = setTimeout(() => {
|
|
245
|
-
cleanup();
|
|
246
|
-
resolve({
|
|
247
|
-
content: [
|
|
248
|
-
{
|
|
249
|
-
type: 'text',
|
|
250
|
-
text: JSON.stringify({
|
|
251
|
-
message: null,
|
|
252
|
-
timeout: true,
|
|
253
|
-
elapsed_ms: Date.now() - startTime,
|
|
254
|
-
}),
|
|
255
|
-
},
|
|
256
|
-
],
|
|
257
|
-
});
|
|
258
|
-
}, actualTimeout);
|
|
259
|
-
}
|
|
260
|
-
});
|
|
261
|
-
} catch (error) {
|
|
262
|
-
return {
|
|
263
|
-
content: [{ type: 'text', text: `Error listening: ${error.message}` }],
|
|
264
|
-
isError: true,
|
|
265
|
-
};
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
);
|
|
269
|
-
|
|
270
|
-
// Tool: List channels
|
|
271
|
-
server.tool(
|
|
272
|
-
'agentchat_channels',
|
|
273
|
-
'List available channels on the connected server',
|
|
274
|
-
{},
|
|
275
|
-
async () => {
|
|
276
|
-
try {
|
|
277
|
-
if (!client || !client.connected) {
|
|
278
|
-
return {
|
|
279
|
-
content: [{ type: 'text', text: 'Not connected. Use agentchat_connect first.' }],
|
|
280
|
-
isError: true,
|
|
281
|
-
};
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
const channels = await client.listChannels();
|
|
285
|
-
|
|
286
|
-
return {
|
|
287
|
-
content: [
|
|
288
|
-
{
|
|
289
|
-
type: 'text',
|
|
290
|
-
text: JSON.stringify({
|
|
291
|
-
channels,
|
|
292
|
-
joined: Array.from(client.channels),
|
|
293
|
-
}),
|
|
294
|
-
},
|
|
295
|
-
],
|
|
296
|
-
};
|
|
297
|
-
} catch (error) {
|
|
298
|
-
return {
|
|
299
|
-
content: [{ type: 'text', text: `Error listing channels: ${error.message}` }],
|
|
300
|
-
isError: true,
|
|
301
|
-
};
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
);
|
|
305
|
-
|
|
306
|
-
// Tool: Start daemon
|
|
307
|
-
server.tool(
|
|
308
|
-
'agentchat_daemon_start',
|
|
309
|
-
'Start a background daemon for persistent AgentChat connection',
|
|
310
|
-
{
|
|
311
|
-
server_url: z.string().describe('WebSocket URL of the AgentChat server'),
|
|
312
|
-
channels: z.array(z.string()).optional().default(['#general']).describe('Channels to join'),
|
|
313
|
-
identity_path: z.string().optional().describe('Path to identity file'),
|
|
314
|
-
instance: z.string().optional().default('default').describe('Daemon instance name'),
|
|
315
|
-
},
|
|
316
|
-
async ({ server_url, channels, identity_path, instance }) => {
|
|
317
|
-
try {
|
|
318
|
-
// Check if already running
|
|
319
|
-
if (await isDaemonRunning(instance)) {
|
|
320
|
-
return {
|
|
321
|
-
content: [
|
|
322
|
-
{
|
|
323
|
-
type: 'text',
|
|
324
|
-
text: JSON.stringify({
|
|
325
|
-
success: false,
|
|
326
|
-
error: `Daemon instance '${instance}' is already running`,
|
|
327
|
-
}),
|
|
328
|
-
},
|
|
329
|
-
],
|
|
330
|
-
};
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
const daemonOptions = {
|
|
334
|
-
server: server_url,
|
|
335
|
-
channels,
|
|
336
|
-
identity: identity_path || DEFAULT_IDENTITY_PATH,
|
|
337
|
-
instance,
|
|
338
|
-
};
|
|
339
|
-
|
|
340
|
-
daemon = new AgentChatDaemon(daemonOptions);
|
|
341
|
-
await daemon.start();
|
|
342
|
-
|
|
343
|
-
const paths = getDaemonPaths(instance);
|
|
344
|
-
|
|
345
|
-
return {
|
|
346
|
-
content: [
|
|
347
|
-
{
|
|
348
|
-
type: 'text',
|
|
349
|
-
text: JSON.stringify({
|
|
350
|
-
success: true,
|
|
351
|
-
instance,
|
|
352
|
-
server: server_url,
|
|
353
|
-
channels,
|
|
354
|
-
inbox: paths.inbox,
|
|
355
|
-
outbox: paths.outbox,
|
|
356
|
-
}),
|
|
357
|
-
},
|
|
358
|
-
],
|
|
359
|
-
};
|
|
360
|
-
} catch (error) {
|
|
361
|
-
return {
|
|
362
|
-
content: [{ type: 'text', text: `Error starting daemon: ${error.message}` }],
|
|
363
|
-
isError: true,
|
|
364
|
-
};
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
);
|
|
368
|
-
|
|
369
|
-
// Tool: Stop daemon
|
|
370
|
-
server.tool(
|
|
371
|
-
'agentchat_daemon_stop',
|
|
372
|
-
'Stop the background AgentChat daemon',
|
|
373
|
-
{
|
|
374
|
-
instance: z.string().optional().default('default').describe('Daemon instance name'),
|
|
375
|
-
},
|
|
376
|
-
async ({ instance }) => {
|
|
377
|
-
try {
|
|
378
|
-
const result = await stopDaemon(instance);
|
|
379
|
-
|
|
380
|
-
// Also stop local daemon reference
|
|
381
|
-
if (daemon) {
|
|
382
|
-
await daemon.stop();
|
|
383
|
-
daemon = null;
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
return {
|
|
387
|
-
content: [
|
|
388
|
-
{
|
|
389
|
-
type: 'text',
|
|
390
|
-
text: JSON.stringify({
|
|
391
|
-
success: true,
|
|
392
|
-
message: result,
|
|
393
|
-
instance,
|
|
394
|
-
}),
|
|
395
|
-
},
|
|
396
|
-
],
|
|
397
|
-
};
|
|
398
|
-
} catch (error) {
|
|
399
|
-
return {
|
|
400
|
-
content: [{ type: 'text', text: `Error stopping daemon: ${error.message}` }],
|
|
401
|
-
isError: true,
|
|
402
|
-
};
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
);
|
|
406
|
-
|
|
407
|
-
// Tool: Read inbox
|
|
408
|
-
server.tool(
|
|
409
|
-
'agentchat_inbox',
|
|
410
|
-
'Read messages from the daemon inbox',
|
|
411
|
-
{
|
|
412
|
-
lines: z.number().optional().default(50).describe('Number of recent lines to read'),
|
|
413
|
-
instance: z.string().optional().default('default').describe('Daemon instance name'),
|
|
414
|
-
},
|
|
415
|
-
async ({ lines, instance }) => {
|
|
416
|
-
try {
|
|
417
|
-
const paths = getDaemonPaths(instance);
|
|
418
|
-
|
|
419
|
-
if (!fs.existsSync(paths.inbox)) {
|
|
420
|
-
return {
|
|
421
|
-
content: [
|
|
422
|
-
{
|
|
423
|
-
type: 'text',
|
|
424
|
-
text: JSON.stringify({
|
|
425
|
-
messages: [],
|
|
426
|
-
error: 'Inbox file not found. Is the daemon running?',
|
|
427
|
-
}),
|
|
428
|
-
},
|
|
429
|
-
],
|
|
430
|
-
};
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
const content = fs.readFileSync(paths.inbox, 'utf-8');
|
|
434
|
-
const allLines = content.trim().split('\n').filter(Boolean);
|
|
435
|
-
const recentLines = allLines.slice(-lines);
|
|
436
|
-
|
|
437
|
-
const messages = [];
|
|
438
|
-
for (const line of recentLines) {
|
|
439
|
-
try {
|
|
440
|
-
messages.push(JSON.parse(line));
|
|
441
|
-
} catch {
|
|
442
|
-
// Skip invalid JSON lines
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
return {
|
|
447
|
-
content: [
|
|
448
|
-
{
|
|
449
|
-
type: 'text',
|
|
450
|
-
text: JSON.stringify({
|
|
451
|
-
messages,
|
|
452
|
-
total_lines: allLines.length,
|
|
453
|
-
returned_lines: messages.length,
|
|
454
|
-
}),
|
|
455
|
-
},
|
|
456
|
-
],
|
|
457
|
-
};
|
|
458
|
-
} catch (error) {
|
|
459
|
-
return {
|
|
460
|
-
content: [{ type: 'text', text: `Error reading inbox: ${error.message}` }],
|
|
461
|
-
isError: true,
|
|
462
|
-
};
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
);
|
|
22
|
+
// Register all tools
|
|
23
|
+
registerAllTools(server);
|
|
466
24
|
|
|
467
25
|
return server;
|
|
468
26
|
}
|