@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.
Files changed (2) hide show
  1. package/index.js +4 -446
  2. 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 { z } from 'zod';
11
- import { AgentChatClient } from '@tjamescouch/agentchat';
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
- // Tool: Connect to server
46
- server.tool(
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tjamescouch/agentchat-mcp",
3
- "version": "0.5.0",
3
+ "version": "0.6.2",
4
4
  "description": "MCP server for AgentChat - real-time AI agent communication",
5
5
  "main": "index.js",
6
6
  "type": "module",