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