mcp-chat-connect 1.0.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 +367 -0
  2. package/package.json +35 -0
package/index.js ADDED
@@ -0,0 +1,367 @@
1
+ #!/usr/bin/env node
2
+
3
+ const http = require('http');
4
+ const { URL } = require('url');
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+
8
+ // Config file stores auth state between sessions
9
+ const CONFIG_DIR = path.join(require('os').homedir(), '.mcp-chat');
10
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
11
+
12
+ const MCP_CHAT_URL = process.env.MCP_CHAT_URL || 'https://mcpchat.dovito.com';
13
+
14
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
15
+
16
+ function escapeHtml(str) {
17
+ if (!str) return '';
18
+ return String(str)
19
+ .replace(/&/g, '&')
20
+ .replace(/</g, '&lt;')
21
+ .replace(/>/g, '&gt;')
22
+ .replace(/"/g, '&quot;')
23
+ .replace(/'/g, '&#39;');
24
+ }
25
+
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
+ // ─── Config persistence ──────────────────────────────────────────────────────
36
+
37
+ function loadConfig() {
38
+ try {
39
+ return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
40
+ } catch {
41
+ return {};
42
+ }
43
+ }
44
+
45
+ function saveConfig(config) {
46
+ if (!fs.existsSync(CONFIG_DIR)) fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
47
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), { mode: 0o600 });
48
+ }
49
+
50
+ // ─── API helpers ─────────────────────────────────────────────────────────────
51
+
52
+ async function apiCall(tool, args, token) {
53
+ const response = await fetch(`${MCP_CHAT_URL}/mcp/call`, {
54
+ method: 'POST',
55
+ headers: {
56
+ 'Content-Type': 'application/json',
57
+ 'Authorization': `Bearer ${token}`,
58
+ },
59
+ body: JSON.stringify({ tool, args }),
60
+ });
61
+ if (!response.ok) {
62
+ throw new Error(`API error: ${response.status} ${response.statusText}`);
63
+ }
64
+ return response.json();
65
+ }
66
+
67
+ // ─── Browser auth flow ───────────────────────────────────────────────────────
68
+
69
+ function startAuthFlow() {
70
+ return new Promise((resolve, reject) => {
71
+ const server = http.createServer((req, res) => {
72
+ const url = new URL(req.url, 'http://localhost');
73
+
74
+ if (url.pathname === '/callback') {
75
+ const token = url.searchParams.get('token');
76
+ const channelId = url.searchParams.get('channel_id');
77
+ const channelName = url.searchParams.get('channel_name');
78
+ const userName = url.searchParams.get('user_name');
79
+
80
+ // Validate inputs
81
+ const parsedChannelId = parseInt(channelId, 10);
82
+ if (!token || !channelId || isNaN(parsedChannelId) || parsedChannelId <= 0) {
83
+ res.writeHead(400, { 'Content-Type': 'text/plain' });
84
+ res.end('Invalid callback parameters');
85
+ return;
86
+ }
87
+
88
+ // Sanitize all values before inserting into HTML
89
+ const safeChannelName = escapeHtml(channelName || channelId);
90
+ const safeRedirectUrl = escapeHtml(`${MCP_CHAT_URL}/chat/${parsedChannelId}`);
91
+
92
+ res.writeHead(200, {
93
+ 'Content-Type': 'text/html',
94
+ 'Content-Security-Policy': "default-src 'none'; style-src 'unsafe-inline'",
95
+ });
96
+ res.end(`<!DOCTYPE html>
97
+ <html><head><title>MCP Chat - Connected</title>
98
+ <meta http-equiv="refresh" content="2;url=${safeRedirectUrl}">
99
+ </head>
100
+ <body style="font-family: system-ui; display: flex; align-items: center; justify-content: center; min-height: 100vh; margin: 0; background: #f8fafc;">
101
+ <div style="text-align: center; max-width: 400px;">
102
+ <h1 style="color: #0f172a;">Connected!</h1>
103
+ <p style="color: #64748b;">Your Claude Code session is now connected to <strong>#${safeChannelName}</strong>.</p>
104
+ <p style="color: #64748b; font-size: 14px;">You can close this tab and return to your terminal.</p>
105
+ </div></body></html>`);
106
+
107
+ server.close();
108
+ resolve({ token, channelId: parsedChannelId, channelName: channelName || '', userName: userName || '' });
109
+ return;
110
+ }
111
+
112
+ res.writeHead(404);
113
+ res.end();
114
+ });
115
+
116
+ // Bind to loopback only
117
+ server.listen(0, '127.0.0.1', async () => {
118
+ const port = server.address().port;
119
+ const connectUrl = `${MCP_CHAT_URL}/connect?callback=${encodeURIComponent(`http://127.0.0.1:${port}/callback`)}`;
120
+
121
+ try {
122
+ const open = (await import('open')).default;
123
+ await open(connectUrl);
124
+ } catch {
125
+ // Fallback: use platform-specific open command with no shell interpolation
126
+ const { spawn } = require('child_process');
127
+ const cmd = process.platform === 'darwin' ? 'open' : 'xdg-open';
128
+ spawn(cmd, [connectUrl], { stdio: 'ignore', detached: true }).unref();
129
+ }
130
+ });
131
+
132
+ // Timeout after 5 minutes
133
+ setTimeout(() => {
134
+ server.close();
135
+ reject(new Error('Auth flow timed out after 5 minutes'));
136
+ }, 300000);
137
+ });
138
+ }
139
+
140
+ // ─── MCP Protocol (JSON-RPC over stdio) ──────────────────────────────────────
141
+
142
+ let sessionState = {
143
+ token: null,
144
+ channelId: null,
145
+ channelName: null,
146
+ userName: null,
147
+ connected: false,
148
+ };
149
+
150
+ // Load saved config on startup
151
+ const savedConfig = loadConfig();
152
+ if (savedConfig.token) {
153
+ sessionState.token = savedConfig.token;
154
+ sessionState.userName = savedConfig.userName;
155
+ }
156
+
157
+ function sendResponse(id, result) {
158
+ const msg = JSON.stringify({ jsonrpc: '2.0', id, result });
159
+ process.stdout.write(`${msg}\n`);
160
+ }
161
+
162
+ function sendError(id, code, message) {
163
+ const msg = JSON.stringify({ jsonrpc: '2.0', id, error: { code, message } });
164
+ process.stdout.write(`${msg}\n`);
165
+ }
166
+
167
+ function getTools() {
168
+ return [
169
+ {
170
+ name: 'mcp_chat_connect',
171
+ 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.',
174
+ inputSchema: { type: 'object', properties: {}, required: [] },
175
+ },
176
+ {
177
+ name: 'mcp_chat_send',
178
+ description: 'Send a message to your connected MCP Chat channel. Messages are informational or recommendations, never direct orders.',
179
+ inputSchema: {
180
+ type: 'object',
181
+ properties: {
182
+ content: { type: 'string', description: 'Message content' },
183
+ message_type: { type: 'string', enum: ['info', 'recommendation', 'status'], description: 'Type of message (default: info)' },
184
+ },
185
+ required: ['content'],
186
+ },
187
+ },
188
+ {
189
+ name: 'mcp_chat_read',
190
+ description: 'Read recent messages from your connected MCP Chat channel.',
191
+ inputSchema: {
192
+ type: 'object',
193
+ properties: {
194
+ limit: { type: 'number', description: 'Number of messages to fetch (default: 20, max: 100)' },
195
+ },
196
+ },
197
+ },
198
+ {
199
+ name: 'mcp_chat_presence',
200
+ description: 'See who is online and which Claude Code sessions are active in your channel.',
201
+ inputSchema: { type: 'object', properties: {} },
202
+ },
203
+ {
204
+ name: 'mcp_chat_channels',
205
+ description: 'List all MCP Chat channels you are a member of.',
206
+ inputSchema: { type: 'object', properties: {} },
207
+ },
208
+ {
209
+ name: 'mcp_chat_status',
210
+ description: 'Check your current MCP Chat connection status.',
211
+ inputSchema: { type: 'object', properties: {} },
212
+ },
213
+ ];
214
+ }
215
+
216
+ async function handleToolCall(name, args) {
217
+ switch (name) {
218
+ case 'mcp_chat_connect': {
219
+ try {
220
+ const result = await startAuthFlow();
221
+ sessionState = {
222
+ token: result.token,
223
+ channelId: result.channelId,
224
+ channelName: result.channelName,
225
+ userName: result.userName,
226
+ connected: true,
227
+ };
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.` }] };
230
+ } catch (err) {
231
+ return { content: [{ type: 'text', text: `Connection failed: ${err.message}` }], isError: true };
232
+ }
233
+ }
234
+
235
+ case 'mcp_chat_send': {
236
+ if (!sessionState.connected) {
237
+ return { content: [{ type: 'text', text: 'Not connected. Run mcp_chat_connect first.' }], isError: true };
238
+ }
239
+ const content = String(args.content || '').slice(0, 10000);
240
+ if (!content) return { content: [{ type: 'text', text: 'Message content is required.' }], isError: true };
241
+ const messageType = ['info', 'recommendation', 'status'].includes(args.message_type) ? args.message_type : 'info';
242
+ const result = await apiCall('send_message', {
243
+ channel_id: sessionState.channelId,
244
+ content,
245
+ message_type: messageType,
246
+ }, sessionState.token);
247
+ if (result.error) return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
248
+ return { content: [{ type: 'text', text: `Message sent to #${sessionState.channelName}` }] };
249
+ }
250
+
251
+ case 'mcp_chat_read': {
252
+ if (!sessionState.connected) {
253
+ return { content: [{ type: 'text', text: 'Not connected. Run mcp_chat_connect first.' }], isError: true };
254
+ }
255
+ const limit = Math.max(1, Math.min(100, parseInt(args.limit, 10) || 20));
256
+ const result = await apiCall('get_messages', {
257
+ channel_id: sessionState.channelId,
258
+ limit,
259
+ }, sessionState.token);
260
+ if (result.error) return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
261
+ if (!result.messages || result.messages.length === 0) {
262
+ return { content: [{ type: 'text', text: `No messages in #${sessionState.channelName}` }] };
263
+ }
264
+ const formatted = result.messages.map(m =>
265
+ `[${new Date(m.created_at).toLocaleTimeString()}] ${m.user_name}: ${m.content}`
266
+ ).join('\n');
267
+ return { content: [{ type: 'text', text: `Messages in #${sessionState.channelName}:\n${formatted}` }] };
268
+ }
269
+
270
+ case 'mcp_chat_presence': {
271
+ if (!sessionState.connected) {
272
+ return { content: [{ type: 'text', text: 'Not connected. Run mcp_chat_connect first.' }], isError: true };
273
+ }
274
+ const result = await apiCall('get_presence', { channel_id: sessionState.channelId }, sessionState.token);
275
+ if (result.error) return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
276
+ if (!result.sessions || result.sessions.length === 0) {
277
+ return { content: [{ type: 'text', text: `No active sessions in #${sessionState.channelName}` }] };
278
+ }
279
+ const formatted = result.sessions.map(s =>
280
+ `- ${s.user_name} (${s.label || 'Claude session'}) ${s.is_connected ? 'online' : 'offline'}`
281
+ ).join('\n');
282
+ return { content: [{ type: 'text', text: `Active in #${sessionState.channelName}:\n${formatted}` }] };
283
+ }
284
+
285
+ case 'mcp_chat_channels': {
286
+ if (!sessionState.token) {
287
+ return { content: [{ type: 'text', text: 'Not authenticated. Run mcp_chat_connect first.' }], isError: true };
288
+ }
289
+ const result = await apiCall('list_channels', {}, sessionState.token);
290
+ if (result.error) return { content: [{ type: 'text', text: `Error: ${result.error}` }], isError: true };
291
+ if (!result.channels || result.channels.length === 0) {
292
+ return { content: [{ type: 'text', text: 'No channels available.' }] };
293
+ }
294
+ const formatted = result.channels.map(c => `- #${c.name} (ID: ${c.id})${c.description ? ` -- ${c.description}` : ''}`).join('\n');
295
+ return { content: [{ type: 'text', text: `Your channels:\n${formatted}` }] };
296
+ }
297
+
298
+ case 'mcp_chat_status': {
299
+ if (!sessionState.connected) {
300
+ 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
+ }
302
+ return { content: [{ type: 'text', text: `Connected to #${sessionState.channelName} as ${sessionState.userName}` }] };
303
+ }
304
+
305
+ default:
306
+ return { content: [{ type: 'text', text: `Unknown tool: ${name}` }], isError: true };
307
+ }
308
+ }
309
+
310
+ // ─── JSON-RPC message handler ────────────────────────────────────────────────
311
+
312
+ async function handleMessage(msg) {
313
+ const { id, method, params } = msg;
314
+
315
+ switch (method) {
316
+ case 'initialize':
317
+ sendResponse(id, {
318
+ protocolVersion: '2024-11-05',
319
+ capabilities: { tools: {} },
320
+ serverInfo: { name: 'mcp-chat-connect', version: '1.0.0' },
321
+ });
322
+ break;
323
+
324
+ case 'notifications/initialized':
325
+ break;
326
+
327
+ case 'tools/list':
328
+ sendResponse(id, { tools: getTools() });
329
+ break;
330
+
331
+ case 'tools/call': {
332
+ const { name, arguments: args } = params;
333
+ try {
334
+ const result = await handleToolCall(name, args || {});
335
+ sendResponse(id, result);
336
+ } catch (err) {
337
+ sendResponse(id, { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true });
338
+ }
339
+ break;
340
+ }
341
+
342
+ default:
343
+ if (id) sendError(id, -32601, `Method not found: ${method}`);
344
+ }
345
+ }
346
+
347
+ // ─── stdio transport ─────────────────────────────────────────────────────────
348
+
349
+ let buffer = '';
350
+
351
+ process.stdin.setEncoding('utf8');
352
+ process.stdin.on('data', (chunk) => {
353
+ buffer += chunk;
354
+ const lines = buffer.split('\n');
355
+ buffer = lines.pop();
356
+ for (const line of lines) {
357
+ if (line.trim()) {
358
+ try {
359
+ handleMessage(JSON.parse(line));
360
+ } catch (err) {
361
+ process.stderr.write(`Parse error: ${err.message}\n`);
362
+ }
363
+ }
364
+ }
365
+ });
366
+
367
+ process.stdin.on('end', () => process.exit(0));
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
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",
5
+ "main": "index.js",
6
+ "bin": {
7
+ "mcp-chat-connect": "./index.js"
8
+ },
9
+ "keywords": [
10
+ "mcp",
11
+ "claude",
12
+ "claude-code",
13
+ "chat",
14
+ "messaging",
15
+ "ai",
16
+ "development",
17
+ "collaboration"
18
+ ],
19
+ "author": "Dovito",
20
+ "license": "MIT",
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "https://github.com/dovito-dev/mcp-chat.git",
24
+ "directory": "mcp-server"
25
+ },
26
+ "engines": {
27
+ "node": ">=18.0.0"
28
+ },
29
+ "dependencies": {
30
+ "open": "^10.1.0"
31
+ },
32
+ "files": [
33
+ "index.js"
34
+ ]
35
+ }