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.
- package/index.js +119 -22
- 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}.
|
|
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
|
-
|
|
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
|
-
|
|
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: {
|
|
320
|
-
|
|
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', () =>
|
|
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.
|
|
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"
|