mcp-chat-connect 1.0.0 → 1.1.1

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 +124 -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,92 @@ 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
+ // Only push presence for Claude Code sessions (have session_token), not browser refreshes
111
+ if (!data.session_token) return;
112
+ // Don't push own presence events
113
+ if (data.user_id === sessionState.userId) return;
114
+
115
+ pushChannelMessage('mcp-chat', `${data.user_name} ${data.status} #${sessionState.channelName}`, {
116
+ channel: sessionState.channelName,
117
+ event: 'presence',
118
+ user: data.user_name,
119
+ status: data.status,
120
+ });
121
+ }
122
+ } catch (err) {
123
+ process.stderr.write(`[mcp-chat] WebSocket parse error: ${err.message}\n`);
124
+ }
125
+ });
126
+
127
+ ws.on('close', () => {
128
+ process.stderr.write(`[mcp-chat] WebSocket disconnected, reconnecting in 5s...\n`);
129
+ wsReconnectTimeout = setTimeout(connectWebSocket, 5000);
130
+ });
131
+
132
+ ws.on('error', (err) => {
133
+ process.stderr.write(`[mcp-chat] WebSocket error: ${err.message}\n`);
134
+ });
135
+ }
136
+
137
+ function disconnectWebSocket() {
138
+ if (wsReconnectTimeout) clearTimeout(wsReconnectTimeout);
139
+ if (wsConnection) {
140
+ try { wsConnection.close(); } catch {}
141
+ wsConnection = null;
142
+ }
143
+ }
144
+
67
145
  // ─── Browser auth flow ───────────────────────────────────────────────────────
68
146
 
69
147
  function startAuthFlow() {
@@ -77,7 +155,6 @@ function startAuthFlow() {
77
155
  const channelName = url.searchParams.get('channel_name');
78
156
  const userName = url.searchParams.get('user_name');
79
157
 
80
- // Validate inputs
81
158
  const parsedChannelId = parseInt(channelId, 10);
82
159
  if (!token || !channelId || isNaN(parsedChannelId) || parsedChannelId <= 0) {
83
160
  res.writeHead(400, { 'Content-Type': 'text/plain' });
@@ -85,7 +162,6 @@ function startAuthFlow() {
85
162
  return;
86
163
  }
87
164
 
88
- // Sanitize all values before inserting into HTML
89
165
  const safeChannelName = escapeHtml(channelName || channelId);
90
166
  const safeRedirectUrl = escapeHtml(`${MCP_CHAT_URL}/chat/${parsedChannelId}`);
91
167
 
@@ -113,7 +189,6 @@ function startAuthFlow() {
113
189
  res.end();
114
190
  });
115
191
 
116
- // Bind to loopback only
117
192
  server.listen(0, '127.0.0.1', async () => {
118
193
  const port = server.address().port;
119
194
  const connectUrl = `${MCP_CHAT_URL}/connect?callback=${encodeURIComponent(`http://127.0.0.1:${port}/callback`)}`;
@@ -122,14 +197,12 @@ function startAuthFlow() {
122
197
  const open = (await import('open')).default;
123
198
  await open(connectUrl);
124
199
  } catch {
125
- // Fallback: use platform-specific open command with no shell interpolation
126
200
  const { spawn } = require('child_process');
127
201
  const cmd = process.platform === 'darwin' ? 'open' : 'xdg-open';
128
202
  spawn(cmd, [connectUrl], { stdio: 'ignore', detached: true }).unref();
129
203
  }
130
204
  });
131
205
 
132
- // Timeout after 5 minutes
133
206
  setTimeout(() => {
134
207
  server.close();
135
208
  reject(new Error('Auth flow timed out after 5 minutes'));
@@ -144,6 +217,7 @@ let sessionState = {
144
217
  channelId: null,
145
218
  channelName: null,
146
219
  userName: null,
220
+ userId: null,
147
221
  connected: false,
148
222
  };
149
223
 
@@ -152,6 +226,7 @@ const savedConfig = loadConfig();
152
226
  if (savedConfig.token) {
153
227
  sessionState.token = savedConfig.token;
154
228
  sessionState.userName = savedConfig.userName;
229
+ sessionState.userId = savedConfig.userId;
155
230
  }
156
231
 
157
232
  function sendResponse(id, result) {
@@ -169,8 +244,8 @@ function getTools() {
169
244
  {
170
245
  name: 'mcp_chat_connect',
171
246
  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.',
247
+ ? `Currently connected to #${sessionState.channelName} as ${sessionState.userName}. Live messages are being pushed into this session. Run again to switch channels.`
248
+ : '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
249
  inputSchema: { type: 'object', properties: {}, required: [] },
175
250
  },
176
251
  {
@@ -217,16 +292,32 @@ async function handleToolCall(name, args) {
217
292
  switch (name) {
218
293
  case 'mcp_chat_connect': {
219
294
  try {
295
+ // Disconnect existing WebSocket if switching channels
296
+ disconnectWebSocket();
297
+
220
298
  const result = await startAuthFlow();
299
+
300
+ // Decode the JWT to get userId (base64 payload)
301
+ let userId = null;
302
+ try {
303
+ const payload = JSON.parse(Buffer.from(result.token.split('.')[1], 'base64').toString());
304
+ userId = payload.id;
305
+ } catch {}
306
+
221
307
  sessionState = {
222
308
  token: result.token,
223
309
  channelId: result.channelId,
224
310
  channelName: result.channelName,
225
311
  userName: result.userName,
312
+ userId,
226
313
  connected: true,
227
314
  };
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.` }] };
315
+ saveConfig({ token: result.token, userName: result.userName, userId });
316
+
317
+ // Start WebSocket listener for real-time push
318
+ connectWebSocket();
319
+
320
+ 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
321
  } catch (err) {
231
322
  return { content: [{ type: 'text', text: `Connection failed: ${err.message}` }], isError: true };
232
323
  }
@@ -299,7 +390,8 @@ async function handleToolCall(name, args) {
299
390
  if (!sessionState.connected) {
300
391
  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
392
  }
302
- return { content: [{ type: 'text', text: `Connected to #${sessionState.channelName} as ${sessionState.userName}` }] };
393
+ const wsStatus = wsConnection?.readyState === 1 ? 'live (receiving messages)' : 'reconnecting...';
394
+ return { content: [{ type: 'text', text: `Connected to #${sessionState.channelName} as ${sessionState.userName}\nWebSocket: ${wsStatus}` }] };
303
395
  }
304
396
 
305
397
  default:
@@ -316,8 +408,11 @@ async function handleMessage(msg) {
316
408
  case 'initialize':
317
409
  sendResponse(id, {
318
410
  protocolVersion: '2024-11-05',
319
- capabilities: { tools: {} },
320
- serverInfo: { name: 'mcp-chat-connect', version: '1.0.0' },
411
+ capabilities: {
412
+ tools: {},
413
+ experimental: { 'claude/channel': {} },
414
+ },
415
+ serverInfo: { name: 'mcp-chat-connect', version: '1.1.0' },
321
416
  });
322
417
  break;
323
418
 
@@ -364,4 +459,11 @@ process.stdin.on('data', (chunk) => {
364
459
  }
365
460
  });
366
461
 
367
- process.stdin.on('end', () => process.exit(0));
462
+ process.stdin.on('end', () => {
463
+ disconnectWebSocket();
464
+ process.exit(0);
465
+ });
466
+
467
+ // Clean shutdown
468
+ process.on('SIGTERM', () => { disconnectWebSocket(); process.exit(0); });
469
+ 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.1",
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"