mcp-chat-connect 1.0.0 → 1.1.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.
Files changed (2) hide show
  1. package/index.js +119 -22
  2. package/package.json +4 -3
package/index.js CHANGED
@@ -4,6 +4,7 @@ const http = require('http');
4
4
  const { URL } = require('url');
5
5
  const fs = require('fs');
6
6
  const path = require('path');
7
+ const WebSocket = require('ws');
7
8
 
8
9
  // Config file stores auth state between sessions
9
10
  const CONFIG_DIR = path.join(require('os').homedir(), '.mcp-chat');
@@ -23,15 +24,6 @@ function escapeHtml(str) {
23
24
  .replace(/'/g, ''');
24
25
  }
25
26
 
26
- function isValidUrl(str) {
27
- try {
28
- const url = new URL(str);
29
- return url.protocol === 'https:' || url.protocol === 'http:';
30
- } catch {
31
- return false;
32
- }
33
- }
34
-
35
27
  // ─── Config persistence ──────────────────────────────────────────────────────
36
28
 
37
29
  function loadConfig() {
@@ -64,6 +56,87 @@ async function apiCall(tool, args, token) {
64
56
  return response.json();
65
57
  }
66
58
 
59
+ // ─── Channel notification (push messages into Claude's context) ──────────────
60
+
61
+ function sendNotification(method, params) {
62
+ const msg = JSON.stringify({ jsonrpc: '2.0', method, params });
63
+ process.stdout.write(`${msg}\n`);
64
+ }
65
+
66
+ function pushChannelMessage(source, content, meta) {
67
+ sendNotification('notifications/claude/channel', {
68
+ content,
69
+ meta: { source, ...meta },
70
+ });
71
+ }
72
+
73
+ // ─── WebSocket listener for real-time channel messages ───────────────────────
74
+
75
+ let wsConnection = null;
76
+ let wsReconnectTimeout = null;
77
+
78
+ function connectWebSocket() {
79
+ if (!sessionState.connected || !sessionState.token || !sessionState.channelId) return;
80
+
81
+ const wsUrl = `${MCP_CHAT_URL.replace('https://', 'wss://').replace('http://', 'ws://')}/ws?token=${sessionState.token}&channel=${sessionState.channelId}&session=mcp-cli`;
82
+
83
+ if (wsConnection) {
84
+ try { wsConnection.close(); } catch {}
85
+ }
86
+
87
+ const ws = new WebSocket(wsUrl);
88
+ wsConnection = ws;
89
+
90
+ ws.on('open', () => {
91
+ process.stderr.write(`[mcp-chat] WebSocket connected to #${sessionState.channelName}\n`);
92
+ });
93
+
94
+ ws.on('message', (raw) => {
95
+ try {
96
+ const data = JSON.parse(raw.toString());
97
+
98
+ if (data.type === 'new_message') {
99
+ const msg = data.message;
100
+ // Don't echo back messages sent by this user's session
101
+ if (msg.user_id === sessionState.userId) return;
102
+
103
+ pushChannelMessage('mcp-chat', msg.content, {
104
+ channel: sessionState.channelName,
105
+ user: msg.user_name || 'unknown',
106
+ message_type: msg.message_type || 'info',
107
+ timestamp: msg.created_at || new Date().toISOString(),
108
+ });
109
+ } else if (data.type === 'presence') {
110
+ pushChannelMessage('mcp-chat', `${data.user_name} ${data.status} #${sessionState.channelName}`, {
111
+ channel: sessionState.channelName,
112
+ event: 'presence',
113
+ user: data.user_name,
114
+ status: data.status,
115
+ });
116
+ }
117
+ } catch (err) {
118
+ process.stderr.write(`[mcp-chat] WebSocket parse error: ${err.message}\n`);
119
+ }
120
+ });
121
+
122
+ ws.on('close', () => {
123
+ process.stderr.write(`[mcp-chat] WebSocket disconnected, reconnecting in 5s...\n`);
124
+ wsReconnectTimeout = setTimeout(connectWebSocket, 5000);
125
+ });
126
+
127
+ ws.on('error', (err) => {
128
+ process.stderr.write(`[mcp-chat] WebSocket error: ${err.message}\n`);
129
+ });
130
+ }
131
+
132
+ function disconnectWebSocket() {
133
+ if (wsReconnectTimeout) clearTimeout(wsReconnectTimeout);
134
+ if (wsConnection) {
135
+ try { wsConnection.close(); } catch {}
136
+ wsConnection = null;
137
+ }
138
+ }
139
+
67
140
  // ─── Browser auth flow ───────────────────────────────────────────────────────
68
141
 
69
142
  function startAuthFlow() {
@@ -77,7 +150,6 @@ function startAuthFlow() {
77
150
  const channelName = url.searchParams.get('channel_name');
78
151
  const userName = url.searchParams.get('user_name');
79
152
 
80
- // Validate inputs
81
153
  const parsedChannelId = parseInt(channelId, 10);
82
154
  if (!token || !channelId || isNaN(parsedChannelId) || parsedChannelId <= 0) {
83
155
  res.writeHead(400, { 'Content-Type': 'text/plain' });
@@ -85,7 +157,6 @@ function startAuthFlow() {
85
157
  return;
86
158
  }
87
159
 
88
- // Sanitize all values before inserting into HTML
89
160
  const safeChannelName = escapeHtml(channelName || channelId);
90
161
  const safeRedirectUrl = escapeHtml(`${MCP_CHAT_URL}/chat/${parsedChannelId}`);
91
162
 
@@ -113,7 +184,6 @@ function startAuthFlow() {
113
184
  res.end();
114
185
  });
115
186
 
116
- // Bind to loopback only
117
187
  server.listen(0, '127.0.0.1', async () => {
118
188
  const port = server.address().port;
119
189
  const connectUrl = `${MCP_CHAT_URL}/connect?callback=${encodeURIComponent(`http://127.0.0.1:${port}/callback`)}`;
@@ -122,14 +192,12 @@ function startAuthFlow() {
122
192
  const open = (await import('open')).default;
123
193
  await open(connectUrl);
124
194
  } catch {
125
- // Fallback: use platform-specific open command with no shell interpolation
126
195
  const { spawn } = require('child_process');
127
196
  const cmd = process.platform === 'darwin' ? 'open' : 'xdg-open';
128
197
  spawn(cmd, [connectUrl], { stdio: 'ignore', detached: true }).unref();
129
198
  }
130
199
  });
131
200
 
132
- // Timeout after 5 minutes
133
201
  setTimeout(() => {
134
202
  server.close();
135
203
  reject(new Error('Auth flow timed out after 5 minutes'));
@@ -144,6 +212,7 @@ let sessionState = {
144
212
  channelId: null,
145
213
  channelName: null,
146
214
  userName: null,
215
+ userId: null,
147
216
  connected: false,
148
217
  };
149
218
 
@@ -152,6 +221,7 @@ const savedConfig = loadConfig();
152
221
  if (savedConfig.token) {
153
222
  sessionState.token = savedConfig.token;
154
223
  sessionState.userName = savedConfig.userName;
224
+ sessionState.userId = savedConfig.userId;
155
225
  }
156
226
 
157
227
  function sendResponse(id, result) {
@@ -169,8 +239,8 @@ function getTools() {
169
239
  {
170
240
  name: 'mcp_chat_connect',
171
241
  description: sessionState.connected
172
- ? `Currently connected to #${sessionState.channelName} as ${sessionState.userName}. Run this again to switch channels.`
173
- : 'Connect to MCP Chat. Opens your browser to authenticate and select a channel.',
242
+ ? `Currently connected to #${sessionState.channelName} as ${sessionState.userName}. Live messages are being pushed into this session. Run again to switch channels.`
243
+ : 'Connect to MCP Chat. Opens your browser to authenticate and select a channel. Once connected, messages will be pushed into this session in real-time.',
174
244
  inputSchema: { type: 'object', properties: {}, required: [] },
175
245
  },
176
246
  {
@@ -217,16 +287,32 @@ async function handleToolCall(name, args) {
217
287
  switch (name) {
218
288
  case 'mcp_chat_connect': {
219
289
  try {
290
+ // Disconnect existing WebSocket if switching channels
291
+ disconnectWebSocket();
292
+
220
293
  const result = await startAuthFlow();
294
+
295
+ // Decode the JWT to get userId (base64 payload)
296
+ let userId = null;
297
+ try {
298
+ const payload = JSON.parse(Buffer.from(result.token.split('.')[1], 'base64').toString());
299
+ userId = payload.id;
300
+ } catch {}
301
+
221
302
  sessionState = {
222
303
  token: result.token,
223
304
  channelId: result.channelId,
224
305
  channelName: result.channelName,
225
306
  userName: result.userName,
307
+ userId,
226
308
  connected: true,
227
309
  };
228
- saveConfig({ token: result.token, userName: result.userName });
229
- return { content: [{ type: 'text', text: `Connected to #${result.channelName} as ${result.userName}. You can now send and read messages.` }] };
310
+ saveConfig({ token: result.token, userName: result.userName, userId });
311
+
312
+ // Start WebSocket listener for real-time push
313
+ connectWebSocket();
314
+
315
+ return { content: [{ type: 'text', text: `Connected to #${result.channelName} as ${result.userName}. Live messages will now be pushed into this session. You can also use mcp_chat_send to send messages and mcp_chat_read to fetch history.` }] };
230
316
  } catch (err) {
231
317
  return { content: [{ type: 'text', text: `Connection failed: ${err.message}` }], isError: true };
232
318
  }
@@ -299,7 +385,8 @@ async function handleToolCall(name, args) {
299
385
  if (!sessionState.connected) {
300
386
  return { content: [{ type: 'text', text: sessionState.token ? 'Authenticated but not connected to a channel. Run mcp_chat_connect to pick a channel.' : 'Not connected. Run mcp_chat_connect to authenticate and select a channel.' }] };
301
387
  }
302
- return { content: [{ type: 'text', text: `Connected to #${sessionState.channelName} as ${sessionState.userName}` }] };
388
+ const wsStatus = wsConnection?.readyState === 1 ? 'live (receiving messages)' : 'reconnecting...';
389
+ return { content: [{ type: 'text', text: `Connected to #${sessionState.channelName} as ${sessionState.userName}\nWebSocket: ${wsStatus}` }] };
303
390
  }
304
391
 
305
392
  default:
@@ -316,8 +403,11 @@ async function handleMessage(msg) {
316
403
  case 'initialize':
317
404
  sendResponse(id, {
318
405
  protocolVersion: '2024-11-05',
319
- capabilities: { tools: {} },
320
- serverInfo: { name: 'mcp-chat-connect', version: '1.0.0' },
406
+ capabilities: {
407
+ tools: {},
408
+ experimental: { 'claude/channel': {} },
409
+ },
410
+ serverInfo: { name: 'mcp-chat-connect', version: '1.1.0' },
321
411
  });
322
412
  break;
323
413
 
@@ -364,4 +454,11 @@ process.stdin.on('data', (chunk) => {
364
454
  }
365
455
  });
366
456
 
367
- process.stdin.on('end', () => process.exit(0));
457
+ process.stdin.on('end', () => {
458
+ disconnectWebSocket();
459
+ process.exit(0);
460
+ });
461
+
462
+ // Clean shutdown
463
+ process.on('SIGTERM', () => { disconnectWebSocket(); process.exit(0); });
464
+ process.on('SIGINT', () => { disconnectWebSocket(); process.exit(0); });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mcp-chat-connect",
3
- "version": "1.0.0",
4
- "description": "MCP server for connecting Claude Code sessions to MCP Chat -- real-time team messaging for AI-assisted development",
3
+ "version": "1.1.0",
4
+ "description": "MCP server with channels support for connecting Claude Code sessions to MCP Chat -- real-time team messaging for AI-assisted development",
5
5
  "main": "index.js",
6
6
  "bin": {
7
7
  "mcp-chat-connect": "./index.js"
@@ -27,7 +27,8 @@
27
27
  "node": ">=18.0.0"
28
28
  },
29
29
  "dependencies": {
30
- "open": "^10.1.0"
30
+ "open": "^10.1.0",
31
+ "ws": "^8.16.0"
31
32
  },
32
33
  "files": [
33
34
  "index.js"