This is page 2 of 2. Use http://codebase.md/fyimail/whatsapp-mcp2?lines=true&page={x} to view the full context.
# Directory Structure
```
├── .cursor
│ └── rules
│ └── project-rules.mdc
├── .dockerignore
├── .eslintrc.js
├── .gitignore
├── .nvmrc
├── .prettierrc
├── .puppeteer_ws
├── bin
│ └── wweb-mcp.js
├── bin.js
├── Dockerfile
├── eslint.config.js
├── fly.toml
├── jest.config.js
├── LICENSE
├── nodemon.json
├── package-lock.json
├── package.json
├── README.md
├── render.yaml
├── render.yml
├── server.js
├── src
│ ├── api.ts
│ ├── logger.ts
│ ├── main.ts
│ ├── mcp-server.ts
│ ├── middleware
│ │ ├── error-handler.ts
│ │ ├── index.ts
│ │ └── logger.ts
│ ├── minimal-server.ts
│ ├── server.js
│ ├── types.ts
│ ├── whatsapp-api-client.ts
│ ├── whatsapp-client.ts
│ ├── whatsapp-integration.js
│ └── whatsapp-service.ts
├── test
│ ├── setup.ts
│ └── unit
│ ├── api.test.ts
│ ├── mcp-server.test.ts
│ ├── utils.test.ts
│ ├── whatsapp-client.test.ts
│ └── whatsapp-service.test.ts
├── test-local.sh
├── tsconfig.json
├── tsconfig.prod.json
├── tsconfig.test.json
└── whatsapp-integration.zip
```
# Files
--------------------------------------------------------------------------------
/src/server.js:
--------------------------------------------------------------------------------
```javascript
1 | // HTTP server with WhatsApp integration
2 | const http = require('http');
3 | const url = require('url');
4 |
5 | // Import WhatsApp integration (but don't wait for it)
6 | const whatsapp = require('./whatsapp-integration');
7 |
8 | // Direct reference to the WhatsApp client for MCP-compatible endpoints
9 | let whatsappClient = null;
10 |
11 | // Set the WhatsApp client reference when it's ready
12 | whatsapp.onClientReady((client) => {
13 | console.log('[Server] WhatsApp client reference received');
14 | whatsappClient = client;
15 | });
16 |
17 | // Start logging immediately
18 | console.log(`[STARTUP] Starting HTTP server with WhatsApp integration`);
19 | console.log(`[STARTUP] Node version: ${process.version}`);
20 | console.log(`[STARTUP] Platform: ${process.platform}`);
21 | console.log(`[STARTUP] PORT: ${process.env.PORT || 3000}`);
22 |
23 | // Start WhatsApp initialization in the background WITHOUT awaiting
24 | // This is critical - we don't block server startup
25 | setTimeout(() => {
26 | console.log('[STARTUP] Starting WhatsApp client initialization in the background');
27 | whatsapp.initializeWhatsAppClient().catch(err => {
28 | console.error('[STARTUP] Error initializing WhatsApp client:', err);
29 | // Non-blocking - server continues running even if WhatsApp fails
30 | });
31 | }, 2000); // Short delay to ensure server is fully up first
32 |
33 | // Create server with no dependencies
34 | const server = http.createServer((req, res) => {
35 | const url = req.url;
36 | console.log(`[${new Date().toISOString()}] ${req.method} ${url}`);
37 |
38 | // Health check endpoint - handle both with and without trailing space
39 | if (url === '/health' || url === '/health ' || url === '/health%20') {
40 | res.writeHead(200, { 'Content-Type': 'application/json' });
41 | res.end(JSON.stringify({ status: 'ok', timestamp: new Date().toISOString() }));
42 | return;
43 | }
44 |
45 | // Root endpoint
46 | if (url === '/' || url === '/%20') {
47 | res.writeHead(200, { 'Content-Type': 'text/html' });
48 | res.end(`
49 | <html>
50 | <head><title>WhatsApp API Server</title></head>
51 | <body>
52 | <h1>WhatsApp API Server</h1>
53 | <p>Server is running successfully</p>
54 | <p>Server time: ${new Date().toISOString()}</p>
55 | <p>Node version: ${process.version}</p>
56 | <p>Available endpoints:</p>
57 | <ul>
58 | <li><a href="/health">Health Check</a></li>
59 | <li><a href="/status">WhatsApp Status</a></li>
60 | <li><a href="/qr">WhatsApp QR Code</a> (when available)</li>
61 | </ul>
62 | </body>
63 | </html>
64 | `);
65 | return;
66 | }
67 |
68 | // WhatsApp Status endpoint
69 | if (url === '/status' || url === '/status%20') {
70 | const status = whatsapp.getStatus();
71 | res.writeHead(200, { 'Content-Type': 'application/json' });
72 | res.end(JSON.stringify({
73 | status: status.status,
74 | error: status.error,
75 | timestamp: new Date().toISOString()
76 | }));
77 | return;
78 | }
79 |
80 | // WhatsApp QR Code endpoint
81 | if (url === '/qr' || url === '/qr%20') {
82 | try {
83 | // Async function so we need to handle it carefully
84 | whatsapp.getQRCode().then(qrCode => {
85 | if (!qrCode) {
86 | res.writeHead(404, { 'Content-Type': 'application/json' });
87 | res.end(JSON.stringify({ error: 'QR code not available', status: whatsapp.getStatus().status }));
88 | return;
89 | }
90 |
91 | res.writeHead(200, { 'Content-Type': 'text/html' });
92 | res.end(`
93 | <html>
94 | <head><title>WhatsApp QR Code</title></head>
95 | <body>
96 | <h1>WhatsApp QR Code</h1>
97 | <p>Scan with your WhatsApp mobile app:</p>
98 | <img src="${qrCode}" alt="WhatsApp QR Code" style="max-width: 300px;"/>
99 | <p>Status: ${whatsapp.getStatus().status}</p>
100 | <p><a href="/qr">Refresh</a> | <a href="/status">Check Status</a></p>
101 | </body>
102 | </html>
103 | `);
104 | }).catch(err => {
105 | console.error('[Server] Error generating QR code:', err);
106 | res.writeHead(500, { 'Content-Type': 'application/json' });
107 | res.end(JSON.stringify({ error: 'Failed to generate QR code', details: err.message }));
108 | });
109 | } catch (err) {
110 | res.writeHead(500, { 'Content-Type': 'application/json' });
111 | res.end(JSON.stringify({ error: 'QR code generation error', details: err.message }));
112 | }
113 | return;
114 | }
115 |
116 | // API Key endpoint - simple way to get the current API key
117 | if (url === '/wa-api' || url === '/wa-api/') {
118 | const status = whatsapp.getStatus();
119 | if (status.status === 'ready' && status.apiKey) {
120 | res.writeHead(200, { 'Content-Type': 'text/html' });
121 | res.end(`
122 | <html>
123 | <head><title>WhatsApp API Key</title></head>
124 | <body>
125 | <h1>WhatsApp API Key</h1>
126 | <p>Current status: <strong>${status.status}</strong></p>
127 | <p>API Key: <code>${status.apiKey}</code></p>
128 | <p>MCP command:</p>
129 | <pre>wweb-mcp -m mcp -s local -c api -t command --api-base-url https://whatsapp-integration-u4q0.onrender.com/api --api-key ${status.apiKey}</pre>
130 | </body>
131 | </html>
132 | `);
133 | } else {
134 | res.writeHead(200, { 'Content-Type': 'text/html' });
135 | res.end(`
136 | <html>
137 | <head><title>WhatsApp API Key</title></head>
138 | <body>
139 | <h1>WhatsApp API Key</h1>
140 | <p>Current status: <strong>${status.status}</strong></p>
141 | <p>API Key not available yet. WhatsApp must be in 'ready' state first.</p>
142 | <p><a href="/api">Refresh</a> | <a href="/status">Check Status</a> | <a href="/qr">Scan QR Code</a></p>
143 | </body>
144 | </html>
145 | `);
146 | }
147 | return;
148 | }
149 |
150 | // MCP Tool specific endpoint - status check with API key (required by wweb-mcp)
151 | if (url === '/api/status' || url.startsWith('/api/status?')) {
152 | const status = whatsapp.getStatus();
153 | const clientApiKey = status.apiKey;
154 |
155 | // Only validate API key if client is ready and has an API key
156 | if (status.status === 'ready' && clientApiKey) {
157 | // Extract API key from request (if any)
158 | const urlParams = new URL('http://dummy.com' + req.url).searchParams;
159 | const requestApiKey = urlParams.get('api_key') || urlParams.get('apiKey');
160 | const headerApiKey = req.headers['x-api-key'] || req.headers['authorization'];
161 | const providedApiKey = requestApiKey || (headerApiKey && headerApiKey.replace('Bearer ', ''));
162 |
163 | // Validate API key if provided
164 | if (providedApiKey && providedApiKey !== clientApiKey) {
165 | console.log(`[${new Date().toISOString()}] Invalid API key for /api/status endpoint`);
166 | res.writeHead(401, { 'Content-Type': 'application/json' });
167 | res.end(JSON.stringify({ success: false, error: 'Invalid API key' }));
168 | return;
169 | }
170 | }
171 |
172 | console.log(`[${new Date().toISOString()}] MCP status check: ${status.status}`);
173 | res.writeHead(200, {
174 | 'Content-Type': 'application/json',
175 | 'Access-Control-Allow-Origin': '*'
176 | });
177 | res.end(JSON.stringify({
178 | success: true,
179 | connected: status.status === 'ready',
180 | status: status.status,
181 | error: status.error,
182 | timestamp: new Date().toISOString()
183 | }));
184 | return;
185 | }
186 |
187 | // Debug endpoint for WhatsApp client state
188 | if (url === '/api/debug') {
189 | const status = whatsapp.getStatus();
190 | const clientInfo = {
191 | status: status.status,
192 | connected: status.connected,
193 | authenticated: status.authenticated || false,
194 | clientExists: !!whatsappClient,
195 | clientInfo: whatsappClient ? {
196 | info: whatsappClient.info ? Object.keys(whatsappClient.info) : null,
197 | hasChats: typeof whatsappClient.getChats === 'function',
198 | hasContacts: typeof whatsappClient.getContacts === 'function'
199 | } : null
200 | };
201 |
202 | res.writeHead(200, {
203 | 'Content-Type': 'application/json',
204 | 'Access-Control-Allow-Origin': '*'
205 | });
206 | res.end(JSON.stringify(clientInfo));
207 | return;
208 | }
209 |
210 | // MCP Tool endpoint - get all chats (required by wweb-mcp)
211 | if (url === '/api/chats' || url.startsWith('/api/chats?')) {
212 | const status = whatsapp.getStatus();
213 | const clientApiKey = status.apiKey;
214 |
215 | // Only validate API key if client is ready and has an API key
216 | if (status.status === 'ready' && clientApiKey) {
217 | // Extract API key from request (if any)
218 | const urlParams = new URL('http://dummy.com' + req.url).searchParams;
219 | const requestApiKey = urlParams.get('api_key') || urlParams.get('apiKey');
220 | const headerApiKey = req.headers['x-api-key'] || req.headers['authorization'];
221 | const providedApiKey = requestApiKey || (headerApiKey && headerApiKey.replace('Bearer ', ''));
222 |
223 | // Validate API key if provided
224 | if (providedApiKey && providedApiKey !== clientApiKey) {
225 | console.log(`[${new Date().toISOString()}] Invalid API key for /api/chats endpoint`);
226 | res.writeHead(401, { 'Content-Type': 'application/json' });
227 | res.end(JSON.stringify({ success: false, error: 'Invalid API key' }));
228 | return;
229 | }
230 | }
231 |
232 | // Handle case where WhatsApp is not ready
233 | if (status.status !== 'ready') {
234 | console.log(`[${new Date().toISOString()}] /api/chats called but WhatsApp is not ready. Status: ${status.status}`);
235 | res.writeHead(503, { 'Content-Type': 'application/json' });
236 | res.end(JSON.stringify({
237 | success: false,
238 | error: `WhatsApp not ready. Current status: ${status.status}`,
239 | status: status.status
240 | }));
241 | return;
242 | }
243 |
244 | // Forward the request to the wweb-mcp library
245 | console.log(`[${new Date().toISOString()}] MCP get_chats request forwarded to WhatsApp client`);
246 |
247 | // Check if WhatsApp client reference is valid
248 | if (!whatsappClient) {
249 | console.error(`[${new Date().toISOString()}] WhatsApp client reference is null or undefined`);
250 | res.writeHead(500, { 'Content-Type': 'application/json' });
251 | res.end(JSON.stringify({
252 | success: false,
253 | error: 'WhatsApp client not properly initialized'
254 | }));
255 | return;
256 | }
257 |
258 | // Using whatsapp-web.js getChats() function with timeout
259 | try {
260 | // Create a timeout promise that rejects after 15 seconds
261 | const timeoutPromise = new Promise((_, reject) => {
262 | setTimeout(() => reject(new Error('Request timed out after 15 seconds')), 15000);
263 | });
264 |
265 | // Debug the client's info
266 | console.log(`[${new Date().toISOString()}] WhatsApp client info:`, {
267 | id: whatsappClient.info ? whatsappClient.info.wid : 'unknown',
268 | platform: whatsappClient.info ? whatsappClient.info.platform : 'unknown',
269 | phone: whatsappClient.info ? whatsappClient.info.phone : 'unknown'
270 | });
271 |
272 | // Enhanced implementation of getChats that's more reliable in containerized environments
273 | const getChatsCustom = async () => {
274 | console.log(`[${new Date().toISOString()}] Using enhanced getChats implementation...`);
275 |
276 | // First try the standard getChats method
277 | try {
278 | console.log(`[${new Date().toISOString()}] Attempting primary getChats method...`);
279 | const primaryChats = await whatsappClient.getChats();
280 | if (primaryChats && primaryChats.length > 0) {
281 | console.log(`[${new Date().toISOString()}] Successfully retrieved ${primaryChats.length} chats using primary method`);
282 | return primaryChats;
283 | }
284 | } catch (err) {
285 | console.warn(`[${new Date().toISOString()}] Primary getChats method failed:`, err.message);
286 | }
287 |
288 | // Next try to access the internal _chats collection which might be more stable
289 | if (whatsappClient._chats && whatsappClient._chats.length > 0) {
290 | console.log(`[${new Date().toISOString()}] Found ${whatsappClient._chats.length} chats in internal collection`);
291 | return whatsappClient._chats;
292 | }
293 |
294 | // Next try the store which is another way to access chats
295 | if (whatsappClient.store && typeof whatsappClient.store.getChats === 'function') {
296 | console.log(`[${new Date().toISOString()}] Attempting to get chats from store...`);
297 | try {
298 | const storeChats = await whatsappClient.store.getChats();
299 | if (storeChats && storeChats.length > 0) {
300 | console.log(`[${new Date().toISOString()}] Found ${storeChats.length} chats in store`);
301 | return storeChats;
302 | }
303 | } catch (err) {
304 | console.error(`[${new Date().toISOString()}] Error getting chats from store:`, err);
305 | }
306 | }
307 |
308 | // Try to get chats using direct access to WA-JS methods (more advanced approach)
309 | try {
310 | console.log(`[${new Date().toISOString()}] Attempting to use WA-JS direct access...`);
311 | const wajs = whatsappClient.pupPage ? await whatsappClient.pupPage.evaluate(() => {
312 | return window.WWebJS.getChats().map(c => ({
313 | id: c.id,
314 | name: c.name,
315 | timestamp: c.t,
316 | isGroup: c.isGroup,
317 | unreadCount: c.unreadCount || 0
318 | }));
319 | }) : null;
320 |
321 | if (wajs && wajs.length > 0) {
322 | console.log(`[${new Date().toISOString()}] Found ${wajs.length} chats using WA-JS direct access`);
323 | return wajs.map(chat => ({
324 | id: { _serialized: chat.id },
325 | name: chat.name || '',
326 | isGroup: chat.isGroup,
327 | timestamp: chat.timestamp,
328 | unreadCount: chat.unreadCount
329 | }));
330 | }
331 | } catch (err) {
332 | console.error(`[${new Date().toISOString()}] Error using WA-JS direct access:`, err);
333 | }
334 |
335 | // As a fallback, provide at least one mock chat for MCP compatibility
336 | console.log(`[${new Date().toISOString()}] All methods failed. Falling back to mock chat data`);
337 | return [{
338 | id: { _serialized: 'mock-chat-id-1' },
339 | name: 'Mock Chat (Fallback)',
340 | isGroup: false,
341 | timestamp: Date.now() / 1000,
342 | unreadCount: 0
343 | }];
344 | };
345 |
346 | // Race between the custom chat implementation and the timeout
347 | Promise.race([
348 | getChatsCustom(),
349 | timeoutPromise
350 | ]).then(chats => {
351 | console.log(`[${new Date().toISOString()}] Successfully retrieved ${chats.length} chats`);
352 | // Transform the chats to the format expected by the MCP tool
353 | const formattedChats = chats.map(chat => ({
354 | id: chat.id._serialized,
355 | name: chat.name || '',
356 | isGroup: chat.isGroup,
357 | timestamp: chat.timestamp ? new Date(chat.timestamp * 1000).toISOString() : null,
358 | unreadCount: chat.unreadCount || 0
359 | }));
360 |
361 | res.writeHead(200, {
362 | 'Content-Type': 'application/json',
363 | 'Access-Control-Allow-Origin': '*'
364 | });
365 | res.end(JSON.stringify({
366 | success: true,
367 | chats: formattedChats
368 | }));
369 | }).catch(err => {
370 | console.error(`[${new Date().toISOString()}] Error getting chats:`, err);
371 | res.writeHead(500, { 'Content-Type': 'application/json' });
372 | res.end(JSON.stringify({
373 | success: false,
374 | error: err.message
375 | }));
376 | });
377 | } catch (err) {
378 | console.error(`[${new Date().toISOString()}] Exception getting chats:`, err);
379 | res.writeHead(500, { 'Content-Type': 'application/json' });
380 | res.end(JSON.stringify({
381 | success: false,
382 | error: err.message
383 | }));
384 | }
385 | return;
386 | }
387 |
388 | // MCP Tool endpoint - get messages from a specific chat
389 | if (url.startsWith('/api/messages/')) {
390 | const status = whatsapp.getStatus();
391 | const clientApiKey = status.apiKey;
392 |
393 | // Only validate API key if client is ready and has an API key
394 | if (status.status === 'ready' && clientApiKey) {
395 | // Extract API key from request (if any)
396 | const urlParams = new URL('http://dummy.com' + req.url).searchParams;
397 | const requestApiKey = urlParams.get('api_key') || urlParams.get('apiKey');
398 | const headerApiKey = req.headers['x-api-key'] || req.headers['authorization'];
399 | const providedApiKey = requestApiKey || (headerApiKey && headerApiKey.replace('Bearer ', ''));
400 |
401 | // Validate API key if provided
402 | if (providedApiKey && providedApiKey !== clientApiKey) {
403 | console.log(`[${new Date().toISOString()}] Invalid API key for /api/messages endpoint`);
404 | res.writeHead(401, { 'Content-Type': 'application/json' });
405 | res.end(JSON.stringify({ success: false, error: 'Invalid API key' }));
406 | return;
407 | }
408 | }
409 |
410 | // Handle case where WhatsApp is not ready
411 | if (status.status !== 'ready') {
412 | console.log(`[${new Date().toISOString()}] /api/messages called but WhatsApp is not ready. Status: ${status.status}`);
413 | res.writeHead(503, { 'Content-Type': 'application/json' });
414 | res.end(JSON.stringify({
415 | success: false,
416 | error: `WhatsApp not ready. Current status: ${status.status}`,
417 | status: status.status
418 | }));
419 | return;
420 | }
421 |
422 | // Extract chat ID from URL
423 | const pathParts = url.split('?')[0].split('/');
424 | const chatId = pathParts[3]; // /api/messages/{chatId}
425 |
426 | if (!chatId) {
427 | res.writeHead(400, { 'Content-Type': 'application/json' });
428 | res.end(JSON.stringify({
429 | success: false,
430 | error: 'Missing chat ID in URL'
431 | }));
432 | return;
433 | }
434 |
435 | // Get the limit from query params
436 | const urlParams = new URL('http://dummy.com' + req.url).searchParams;
437 | const limit = parseInt(urlParams.get('limit') || '20', 10);
438 |
439 | // Get messages for this chat
440 | console.log(`[${new Date().toISOString()}] MCP get_messages request for chat ${chatId}`);
441 | try {
442 | // Format chat ID correctly for whatsapp-web.js
443 | const formattedChatId = chatId.includes('@') ? chatId : `${chatId}@c.us`;
444 |
445 | // First get the chat object
446 | whatsappClient.getChatById(formattedChatId).then(chat => {
447 | // Then fetch messages
448 | chat.fetchMessages({ limit }).then(messages => {
449 | // Format the messages as required by the MCP tool
450 | const formattedMessages = messages.map(msg => ({
451 | id: msg.id._serialized,
452 | body: msg.body || '',
453 | timestamp: msg.timestamp ? new Date(msg.timestamp * 1000).toISOString() : null,
454 | from: msg.from || '',
455 | fromMe: msg.fromMe || false,
456 | type: msg.type || 'chat'
457 | }));
458 |
459 | res.writeHead(200, {
460 | 'Content-Type': 'application/json',
461 | 'Access-Control-Allow-Origin': '*'
462 | });
463 | res.end(JSON.stringify({
464 | success: true,
465 | messages: formattedMessages
466 | }));
467 | }).catch(err => {
468 | console.error(`[${new Date().toISOString()}] Error fetching messages:`, err);
469 | res.writeHead(500, { 'Content-Type': 'application/json' });
470 | res.end(JSON.stringify({
471 | success: false,
472 | error: err.message
473 | }));
474 | });
475 | }).catch(err => {
476 | console.error(`[${new Date().toISOString()}] Error getting chat by ID:`, err);
477 | res.writeHead(404, { 'Content-Type': 'application/json' });
478 | res.end(JSON.stringify({
479 | success: false,
480 | error: `Chat not found: ${err.message}`
481 | }));
482 | });
483 | } catch (err) {
484 | console.error(`[${new Date().toISOString()}] Exception getting messages:`, err);
485 | res.writeHead(500, { 'Content-Type': 'application/json' });
486 | res.end(JSON.stringify({
487 | success: false,
488 | error: err.message
489 | }));
490 | }
491 | return;
492 | }
493 |
494 | // Support OPTIONS requests for CORS
495 | if (req.method === 'OPTIONS') {
496 | res.writeHead(200, {
497 | 'Access-Control-Allow-Origin': '*',
498 | 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
499 | 'Access-Control-Allow-Headers': 'Content-Type, Authorization, x-api-key'
500 | });
501 | res.end();
502 | return;
503 | }
504 |
505 | // Add a test message endpoint to validate sending works
506 | if (url === '/api/test-message') {
507 | const status = whatsapp.getStatus();
508 |
509 | // Check if WhatsApp is ready
510 | if (status.status !== 'ready') {
511 | console.log(`[${new Date().toISOString()}] /api/test-message called but WhatsApp is not ready. Status: ${status.status}`);
512 | res.writeHead(503, { 'Content-Type': 'application/json' });
513 | res.end(JSON.stringify({
514 | success: false,
515 | error: `WhatsApp not ready. Current status: ${status.status}`,
516 | status: status.status
517 | }));
518 | return;
519 | }
520 |
521 | // Send a test message to the specified number
522 | const testNumber = '16505578984';
523 | const testMessage = `Test message from WhatsApp API at ${new Date().toISOString()}`;
524 |
525 | console.log(`[${new Date().toISOString()}] Sending test message to ${testNumber}`);
526 |
527 | whatsapp.sendMessage(testNumber, testMessage)
528 | .then(result => {
529 | console.log(`[${new Date().toISOString()}] Test message sent successfully to ${testNumber}`);
530 | res.writeHead(200, { 'Content-Type': 'application/json' });
531 | res.end(JSON.stringify({
532 | success: true,
533 | message: 'Test message sent successfully',
534 | to: testNumber,
535 | messageId: result.id ? result.id._serialized : 'sent',
536 | content: testMessage
537 | }));
538 | })
539 | .catch(err => {
540 | console.error(`[${new Date().toISOString()}] Error sending test message:`, err);
541 | res.writeHead(500, { 'Content-Type': 'application/json' });
542 | res.end(JSON.stringify({
543 | success: false,
544 | error: err.message
545 | }));
546 | });
547 |
548 | return;
549 | }
550 |
551 | // Handle API message send endpoint POST /api/send
552 | if (url === '/api/send' && req.method === 'POST') {
553 | const status = whatsapp.getStatus();
554 | const clientApiKey = status.apiKey;
555 |
556 | // Only validate API key if client is ready and has an API key
557 | if (status.status === 'ready' && clientApiKey) {
558 | // Extract API key from request (if any)
559 | const headerApiKey = req.headers['x-api-key'] || req.headers['authorization'];
560 | const providedApiKey = headerApiKey && headerApiKey.replace('Bearer ', '');
561 |
562 | // Validate API key if provided
563 | if (!providedApiKey || providedApiKey !== clientApiKey) {
564 | console.log(`[${new Date().toISOString()}] Invalid or missing API key for /api/send endpoint`);
565 | res.writeHead(401, { 'Content-Type': 'application/json' });
566 | res.end(JSON.stringify({ success: false, error: 'Invalid or missing API key' }));
567 | return;
568 | }
569 | } else {
570 | // Handle case where WhatsApp is not ready
571 | console.log(`[${new Date().toISOString()}] /api/send called but WhatsApp is not ready. Status: ${status.status}`);
572 | res.writeHead(503, { 'Content-Type': 'application/json' });
573 | res.end(JSON.stringify({
574 | success: false,
575 | error: `WhatsApp not ready. Current status: ${status.status}`,
576 | status: status.status
577 | }));
578 | return;
579 | }
580 |
581 | // Get request body
582 | let body = '';
583 | req.on('data', chunk => {
584 | body += chunk.toString();
585 | });
586 |
587 | req.on('end', async () => {
588 | try {
589 | const data = JSON.parse(body);
590 | const { to, message } = data;
591 |
592 | if (!to || !message) {
593 | res.writeHead(400, { 'Content-Type': 'application/json' });
594 | res.end(JSON.stringify({
595 | success: false,
596 | error: 'Missing required fields: to, message'
597 | }));
598 | return;
599 | }
600 |
601 | console.log(`[${new Date().toISOString()}] Sending message to ${to}`);
602 |
603 | try {
604 | const result = await whatsapp.sendMessage(to, message);
605 | console.log(`[${new Date().toISOString()}] Message sent successfully to ${to}`);
606 |
607 | res.writeHead(200, {
608 | 'Content-Type': 'application/json',
609 | 'Access-Control-Allow-Origin': '*'
610 | });
611 | res.end(JSON.stringify({
612 | success: true,
613 | messageId: result.id ? result.id._serialized : 'sent',
614 | to: result.to || to
615 | }));
616 | } catch (err) {
617 | console.error(`[${new Date().toISOString()}] Error sending message:`, err);
618 | res.writeHead(500, { 'Content-Type': 'application/json' });
619 | res.end(JSON.stringify({
620 | success: false,
621 | error: err.message
622 | }));
623 | }
624 | } catch (err) {
625 | console.error(`[${new Date().toISOString()}] Error parsing JSON:`, err);
626 | res.writeHead(400, { 'Content-Type': 'application/json' });
627 | res.end(JSON.stringify({
628 | success: false,
629 | error: 'Invalid JSON in request body'
630 | }));
631 | }
632 | });
633 | return;
634 | }
635 |
636 | // Handle preflight CORS requests
637 | if (req.method === 'OPTIONS') {
638 | res.writeHead(200, {
639 | 'Access-Control-Allow-Origin': '*',
640 | 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
641 | 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-API-Key',
642 | 'Access-Control-Max-Age': '86400'
643 | });
644 | res.end();
645 | return;
646 | }
647 |
648 | // Add endpoint to get the most recent message from a chat
649 | if (url === '/api/recent-message' || url.startsWith('/api/recent-message?')) {
650 | const status = whatsapp.getStatus();
651 | const clientApiKey = status.apiKey;
652 |
653 | // Only validate API key if client is ready and has an API key
654 | if (status.status === 'ready' && clientApiKey) {
655 | // Extract API key from request (if any)
656 | const urlParams = new URL('http://dummy.com' + req.url).searchParams;
657 | const requestApiKey = urlParams.get('api_key') || urlParams.get('apiKey');
658 | const headerApiKey = req.headers['x-api-key'] || req.headers['authorization'];
659 | const providedApiKey = requestApiKey || (headerApiKey && headerApiKey.replace('Bearer ', ''));
660 |
661 | // Validate API key if provided
662 | if (providedApiKey && providedApiKey !== clientApiKey) {
663 | console.log(`[${new Date().toISOString()}] Invalid API key for /api/recent-message endpoint`);
664 | res.writeHead(401, { 'Content-Type': 'application/json' });
665 | res.end(JSON.stringify({ success: false, error: 'Invalid API key' }));
666 | return;
667 | }
668 | }
669 |
670 | // Handle case where WhatsApp is not ready
671 | if (status.status !== 'ready') {
672 | console.log(`[${new Date().toISOString()}] /api/recent-message called but WhatsApp is not ready. Status: ${status.status}`);
673 | res.writeHead(503, { 'Content-Type': 'application/json' });
674 | res.end(JSON.stringify({
675 | success: false,
676 | error: `WhatsApp not ready. Current status: ${status.status}`,
677 | status: status.status
678 | }));
679 | return;
680 | }
681 |
682 | console.log(`[${new Date().toISOString()}] Getting most recent chat messages...`);
683 |
684 | // Check if WhatsApp client reference is valid
685 | if (!whatsappClient) {
686 | console.error(`[${new Date().toISOString()}] WhatsApp client reference is null or undefined`);
687 | res.writeHead(500, { 'Content-Type': 'application/json' });
688 | res.end(JSON.stringify({
689 | success: false,
690 | error: 'WhatsApp client not properly initialized'
691 | }));
692 | return;
693 | }
694 |
695 | // Using enhanced method to get chats first
696 | // Wrap the whole logic in an async IIFE (Immediately Invoked Function Expression)
697 | (async () => {
698 | try {
699 | // Create a timeout promise
700 | const timeoutPromise = new Promise((_, reject) => {
701 | setTimeout(() => reject(new Error('Request timed out after 15 seconds')), 15000);
702 | });
703 |
704 | // Enhanced method to get recent message
705 | const getRecentMessage = async () => {
706 | try {
707 | // First get chats using our enhanced method
708 | const getChatsCustom = async () => {
709 | // Try the standard getChats method first
710 | try {
711 | const primaryChats = await whatsappClient.getChats();
712 | if (primaryChats && primaryChats.length > 0) {
713 | return primaryChats;
714 | }
715 | } catch (err) {
716 | console.warn(`[${new Date().toISOString()}] Standard getChats failed:`, err.message);
717 | }
718 |
719 | // Try direct access to _chats
720 | if (whatsappClient._chats && whatsappClient._chats.length > 0) {
721 | return whatsappClient._chats;
722 | }
723 |
724 | // Try using store
725 | if (whatsappClient.store && typeof whatsappClient.store.getChats === 'function') {
726 | try {
727 | const storeChats = await whatsappClient.store.getChats();
728 | if (storeChats && storeChats.length > 0) {
729 | return storeChats;
730 | }
731 | } catch (err) {}
732 | }
733 |
734 | // Try WA-JS direct access
735 | try {
736 | const wajs = whatsappClient.pupPage ? await whatsappClient.pupPage.evaluate(() => {
737 | return window.WWebJS.getChats().map(c => ({
738 | id: c.id,
739 | name: c.name,
740 | timestamp: c.t,
741 | isGroup: c.isGroup
742 | }));
743 | }) : null;
744 |
745 | if (wajs && wajs.length > 0) {
746 | return wajs.map(chat => ({
747 | id: { _serialized: chat.id },
748 | name: chat.name || '',
749 | isGroup: chat.isGroup,
750 | timestamp: chat.timestamp
751 | }));
752 | }
753 | } catch (err) {}
754 |
755 | // Fallback
756 | return [];
757 | };
758 |
759 | // Get chats using our enhanced method
760 | const chats = await getChatsCustom();
761 | console.log(`[${new Date().toISOString()}] Retrieved ${chats.length} chats`);
762 |
763 | if (chats.length === 0) {
764 | return { noChats: true };
765 | }
766 |
767 | // Sort chats by timestamp if available
768 | const sortedChats = chats.sort((a, b) => {
769 | const timeA = a.timestamp || 0;
770 | const timeB = b.timestamp || 0;
771 | return timeB - timeA; // Newest first
772 | });
773 |
774 | // Get most recent chat
775 | const recentChat = sortedChats[0];
776 | if (!recentChat || !recentChat.id || !recentChat.id._serialized) {
777 | return { noValidChat: true, chats: sortedChats.length };
778 | }
779 |
780 | // Get messages from this chat
781 | console.log(`[${new Date().toISOString()}] Getting messages from chat: ${recentChat.id._serialized}`);
782 | try {
783 | // Format chat ID correctly for whatsapp-web.js
784 | const formattedChatId = recentChat.id._serialized;
785 | const chat = await whatsappClient.getChatById(formattedChatId);
786 | const messages = await chat.fetchMessages({ limit: 1 });
787 |
788 | if (messages && messages.length > 0) {
789 | return {
790 | success: true,
791 | chat: {
792 | id: recentChat.id._serialized,
793 | name: recentChat.name || '',
794 | isGroup: recentChat.isGroup || false
795 | },
796 | message: {
797 | id: messages[0].id._serialized,
798 | body: messages[0].body || '',
799 | timestamp: messages[0].timestamp ? new Date(messages[0].timestamp * 1000).toISOString() : null,
800 | from: messages[0].from || '',
801 | fromMe: messages[0].fromMe || false
802 | }
803 | };
804 | } else {
805 | return { noMessages: true, chatId: formattedChatId };
806 | }
807 | } catch (err) {
808 | console.error(`[${new Date().toISOString()}] Error getting chat by ID:`, err);
809 | return { chatError: err.message, chatId: recentChat.id._serialized };
810 | }
811 | } catch (err) {
812 | console.error(`[${new Date().toISOString()}] Error in getRecentMessage:`, err);
813 | return { error: err.message };
814 | }
815 | };
816 |
817 | // Race between the fetching and the timeout
818 | const result = await Promise.race([
819 | getRecentMessage(),
820 | timeoutPromise
821 | ]);
822 |
823 | console.log(`[${new Date().toISOString()}] Recent message request result:`, JSON.stringify(result));
824 |
825 | res.writeHead(200, {
826 | 'Content-Type': 'application/json',
827 | 'Access-Control-Allow-Origin': '*'
828 | });
829 | res.end(JSON.stringify(result));
830 | } catch (err) {
831 | console.error(`[${new Date().toISOString()}] Exception getting recent message:`, err);
832 | res.writeHead(500, { 'Content-Type': 'application/json' });
833 | res.end(JSON.stringify({
834 | success: false,
835 | error: err.message
836 | }));
837 | }
838 | })();
839 | return;
840 | }
841 |
842 | // 404 for everything else
843 | res.writeHead(404, { 'Content-Type': 'text/plain' });
844 | res.end('Not Found');
845 | });
846 |
847 | // Listen on all interfaces
848 | const PORT = process.env.PORT || 3000;
849 | server.listen(PORT, '0.0.0.0', () => {
850 | console.log(`[${new Date().toISOString()}] Server listening on port ${PORT}`);
851 | });
852 |
853 | // Handle termination gracefully
854 | process.on('SIGINT', () => {
855 | console.log(`[${new Date().toISOString()}] Server shutting down`);
856 | process.exit(0);
857 | });
858 |
859 | // Handle uncaught exceptions
860 | process.on('uncaughtException', error => {
861 | console.error(`[${new Date().toISOString()}] Uncaught exception: ${error.message}`);
862 | console.error(error.stack);
863 | // Keep server running despite errors
864 | });
865 |
```
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
```typescript
1 | import express, { NextFunction, Request, Response } from 'express';
2 | import { createMcpServer, McpConfig } from './mcp-server';
3 | import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
4 | import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
5 | import { createWhatsAppClient, WhatsAppConfig } from './whatsapp-client';
6 | import yargs from 'yargs';
7 | import { hideBin } from 'yargs/helpers';
8 | import logger, { configureForCommandMode } from './logger';
9 | import { requestLogger, errorHandler } from './middleware';
10 | import { routerFactory } from './api';
11 | import { Client } from 'whatsapp-web.js';
12 | import fs from 'fs';
13 | import path from 'path';
14 | import crypto from 'crypto';
15 |
16 | const isDockerContainer = process.env.DOCKER_CONTAINER === 'true';
17 |
18 | function parseCommandLineArgs(): ReturnType<typeof yargs.parseSync> {
19 | return yargs(hideBin(process.argv))
20 | .option('mode', {
21 | alias: 'm',
22 | description: 'Run mode: mcp or whatsapp-api',
23 | type: 'string',
24 | choices: ['mcp', 'whatsapp-api'],
25 | default: 'mcp',
26 | })
27 | .option('mcp-mode', {
28 | alias: 'c',
29 | description:
30 | 'MCP connection mode: standalone (direct WhatsApp client) or api (connect to WhatsApp API)',
31 | type: 'string',
32 | choices: ['standalone', 'api'],
33 | default: 'standalone',
34 | })
35 | .option('transport', {
36 | alias: 't',
37 | description: 'MCP transport mode: sse or command',
38 | type: 'string',
39 | choices: ['sse', 'command'],
40 | default: 'sse',
41 | })
42 | .option('sse-port', {
43 | alias: 'p',
44 | description: 'Port for SSE server',
45 | type: 'number',
46 | default: 3002,
47 | })
48 | .option('api-port', {
49 | description: 'Port for WhatsApp API server',
50 | type: 'number',
51 | default: 3002,
52 | })
53 | .option('auth-data-path', {
54 | alias: 'a',
55 | description: 'Path to store authentication data',
56 | type: 'string',
57 | default: '.wwebjs_auth',
58 | })
59 | .option('auth-strategy', {
60 | alias: 's',
61 | description: 'Authentication strategy: local or none',
62 | type: 'string',
63 | choices: ['local', 'none'],
64 | default: 'local',
65 | })
66 | .option('api-key', {
67 | alias: 'k',
68 | description: 'API key for WhatsApp Web REST API when using api mode',
69 | type: 'string',
70 | default: '',
71 | })
72 | .option('log-level', {
73 | alias: 'l',
74 | description: 'Log level: error, warn, info, http, debug',
75 | type: 'string',
76 | choices: ['error', 'warn', 'info', 'http', 'debug'],
77 | default: 'info',
78 | })
79 | .help()
80 | .alias('help', 'h')
81 | .parseSync();
82 | }
83 |
84 | function configureLogger(argv: ReturnType<typeof parseCommandLineArgs>): void {
85 | logger.level = argv['log-level'] as string;
86 |
87 | // Configure logger to use stderr for all levels when in MCP command mode
88 | if (argv.mode === 'mcp' && argv.transport === 'command') {
89 | configureForCommandMode();
90 | }
91 | }
92 |
93 | function createConfigurations(argv: ReturnType<typeof parseCommandLineArgs>): {
94 | whatsAppConfig: WhatsAppConfig;
95 | mcpConfig: McpConfig;
96 | } {
97 | const whatsAppConfig: WhatsAppConfig = {
98 | authDir: argv['auth-data-path'] as string,
99 | authStrategy: argv['auth-strategy'] as 'local' | 'none',
100 | dockerContainer: isDockerContainer,
101 | };
102 |
103 | const mcpConfig: McpConfig = {
104 | useApiClient: argv['mcp-mode'] === 'api',
105 | apiKey: argv['api-key'] as string,
106 | whatsappConfig: whatsAppConfig,
107 | };
108 |
109 | return { whatsAppConfig, mcpConfig };
110 | }
111 |
112 | async function startMcpSseServer(
113 | server: ReturnType<typeof createMcpServer>,
114 | port: number,
115 | mode: string,
116 | ): Promise<void> {
117 | const app = express();
118 | app.use(requestLogger);
119 |
120 | let transport: SSEServerTransport;
121 |
122 | app.get('/sse', async (_req: Request, res: Response) => {
123 | logger.info('Received SSE connection');
124 | transport = new SSEServerTransport('/message', res);
125 | await server.connect(transport);
126 | });
127 |
128 | app.post('/message', async (req: Request, res: Response) => {
129 | await transport?.handlePostMessage(req, res);
130 | });
131 |
132 | app.use(errorHandler);
133 |
134 | app.listen(port, '0.0.0.0', () => {
135 | logger.info(`MCP server is running on port ${port} in ${mode} mode`);
136 | });
137 | }
138 |
139 | async function startMcpCommandServer(
140 | server: ReturnType<typeof createMcpServer>,
141 | mode: string,
142 | ): Promise<void> {
143 | try {
144 | const transport = new StdioServerTransport();
145 | await server.connect(transport);
146 | logger.info(`WhatsApp MCP server started successfully in ${mode} mode`);
147 |
148 | process.stdin.on('close', () => {
149 | logger.info('WhatsApp MCP Server closed');
150 | server.close();
151 | });
152 | } catch (error) {
153 | logger.error('Error connecting to MCP server', error);
154 | }
155 | }
156 |
157 | async function getWhatsAppApiKey(whatsAppConfig: WhatsAppConfig): Promise<string> {
158 | if (whatsAppConfig.authStrategy === 'none') {
159 | return crypto.randomBytes(32).toString('hex');
160 | }
161 | const authDataPath = whatsAppConfig.authDir;
162 | if (!authDataPath) {
163 | throw new Error('The auth-data-path is required when using whatsapp-api mode');
164 | }
165 | const apiKeyPath = path.join(authDataPath, 'api_key.txt');
166 | if (!fs.existsSync(apiKeyPath)) {
167 | const apiKey = crypto.randomBytes(32).toString('hex');
168 | fs.writeFileSync(apiKeyPath, apiKey);
169 | return apiKey;
170 | }
171 | return fs.readFileSync(apiKeyPath, 'utf8');
172 | }
173 |
174 | async function startWhatsAppApiServer(whatsAppConfig: WhatsAppConfig, port: number): Promise<void> {
175 | logger.info('[WA] Starting WhatsApp Web REST API...');
176 |
177 | // Create the Express app before initializing WhatsApp client
178 | const app = express();
179 |
180 | // Add error handling to all middleware
181 | app.use((req: Request, res: Response, next: NextFunction) => {
182 | try {
183 | requestLogger(req, res, next);
184 | } catch (error) {
185 | logger.error('[WA] Error in request logger middleware:', error);
186 | next();
187 | }
188 | });
189 |
190 | app.use(express.json());
191 |
192 | // CRITICAL: Track server start time - helps with troubleshooting
193 | const serverStartTime = new Date();
194 |
195 | // Set up minimal state management for diagnostics
196 | const state = {
197 | whatsappInitializing: false,
198 | whatsappInitStarted: false,
199 | whatsappError: null as Error | null,
200 | clientReady: false,
201 | latestQrCode: null as string | null,
202 | client: null as any, // Will hold the WhatsApp client instance once initialized
203 | environment: {
204 | node: process.version,
205 | platform: process.platform,
206 | port: port || process.env.PORT || 3000,
207 | pid: process.pid,
208 | uptime: () => Math.floor((new Date().getTime() - serverStartTime.getTime()) / 1000),
209 | },
210 | };
211 |
212 | // Log important startup information
213 | logger.info(
214 | `[WA] Server starting with Node ${state.environment.node} on ${state.environment.platform}`,
215 | );
216 | logger.info(`[WA] Process ID: ${state.environment.pid}`);
217 | logger.info(`[WA] Port: ${state.environment.port}`);
218 | logger.info(`[WA] Start time: ${serverStartTime.toISOString()}`);
219 |
220 | // EMERGENCY DIAGNOSTIC endpoint - absolutely minimal, will help diagnose deployment issues
221 | app.get('/', (_req: Request, res: Response) => {
222 | res.status(200).send(`
223 | <html>
224 | <head><title>WhatsApp API Service</title></head>
225 | <body>
226 | <h1>WhatsApp API Service</h1>
227 | <p>Server is running</p>
228 | <p>Uptime: ${state.environment.uptime()} seconds</p>
229 | <p>Started: ${serverStartTime.toISOString()}</p>
230 | <p>Node: ${state.environment.node}</p>
231 | <p>Platform: ${state.environment.platform}</p>
232 | <p>WhatsApp Status: ${
233 | state.whatsappInitStarted
234 | ? state.clientReady
235 | ? 'Ready'
236 | : state.whatsappError
237 | ? 'Error'
238 | : 'Initializing'
239 | : 'Not started'
240 | }</p>
241 | <ul>
242 | <li><a href="/health">Health Check</a></li>
243 | <li><a href="/memory-usage">Memory Usage</a></li>
244 | <li><a href="/container-env">Container Environment</a></li>
245 | <li><a href="/filesys">File System Check</a></li>
246 | <li><a href="/qr">QR Code</a> (if available)</li>
247 | </ul>
248 | </body>
249 | </html>
250 | `);
251 | });
252 |
253 | // Add health check endpoint that doesn't require authentication
254 | // CRITICAL: This must be minimal and not depend on any WhatsApp state
255 | app.get('/health', (_req: Request, res: Response) => {
256 | try {
257 | // Always return 200 for Render health check, even if WhatsApp is still initializing
258 | res.status(200).json({
259 | status: 'ok',
260 | server: 'running',
261 | uptime: state.environment.uptime(),
262 | startTime: serverStartTime.toISOString(),
263 | whatsappStarted: state.whatsappInitStarted,
264 | whatsapp: state.clientReady
265 | ? 'ready'
266 | : state.whatsappError
267 | ? 'error'
268 | : state.whatsappInitializing
269 | ? 'initializing'
270 | : 'not_started',
271 | timestamp: new Date().toISOString(),
272 | });
273 | } catch (error) {
274 | logger.error('[WA] Error in health check endpoint:', error);
275 | // Still return 200 to keep Render happy
276 | res.status(200).send('OK');
277 | }
278 | });
279 |
280 | // Add /wa-api endpoint for backwards compatibility with previous implementation
281 | app.get('/wa-api', (_req: Request, res: Response) => {
282 | try {
283 | // Get the API key from the same place as the official implementation
284 | const apiKeyPath = path.join(whatsAppConfig.authDir || '.wwebjs_auth', 'api_key.txt');
285 |
286 | if (fs.existsSync(apiKeyPath)) {
287 | const apiKey = fs.readFileSync(apiKeyPath, 'utf8');
288 | logger.info('[WA] API key retrieved for /wa-api endpoint');
289 |
290 | res.status(200).json({
291 | status: 'success',
292 | message: 'WhatsApp API key',
293 | apiKey: apiKey,
294 | });
295 | } else {
296 | logger.warn('[WA] API key file not found for /wa-api endpoint');
297 | res.status(404).json({
298 | status: 'error',
299 | message: 'API key not found. Service might still be initializing.',
300 | });
301 | }
302 | } catch (error) {
303 | logger.error('[WA] Error retrieving API key for /wa-api endpoint:', error);
304 | res.status(500).json({
305 | status: 'error',
306 | message: 'Failed to retrieve API key',
307 | error: error instanceof Error ? error.message : String(error),
308 | });
309 | }
310 | });
311 |
312 | // Add QR code endpoint with enhanced error handling
313 | app.get('/qr', (_req: Request, res: Response) => {
314 | try {
315 | // First try to get QR from file
316 | try {
317 | const qrPath = path.join('/var/data/whatsapp', 'last-qr.txt');
318 | if (fs.existsSync(qrPath)) {
319 | try {
320 | const qrCode = fs.readFileSync(qrPath, 'utf8');
321 | return res.send(`
322 | <html>
323 | <head>
324 | <title>WhatsApp QR Code</title>
325 | <meta name="viewport" content="width=device-width, initial-scale=1">
326 | <style>
327 | body { font-family: Arial, sans-serif; text-align: center; padding: 20px; }
328 | .qr-container { margin: 20px auto; }
329 | pre { background: #f4f4f4; padding: 20px; display: inline-block; text-align: left; }
330 | .status { color: #555; margin: 20px 0; }
331 | </style>
332 | </head>
333 | <body>
334 | <h1>WhatsApp QR Code</h1>
335 | <p>Scan this QR code with your WhatsApp app to link your device</p>
336 | <div class="qr-container">
337 | <pre>${qrCode}</pre>
338 | </div>
339 | <p class="status">Server status: ${state.whatsappInitializing ? 'Initializing WhatsApp...' : state.clientReady ? 'WhatsApp Ready' : 'Waiting for authentication'}</p>
340 | <p><small>Last updated: ${new Date().toISOString()}</small></p>
341 | <p><a href="/">Back to Home</a></p>
342 | </body>
343 | </html>
344 | `);
345 | } catch (readError) {
346 | logger.error('[WA] Error reading QR file:', readError);
347 | // Continue to fallback methods
348 | }
349 | }
350 | } catch (fileError) {
351 | logger.error('[WA] Error accessing QR file system:', fileError);
352 | // Continue to fallback methods
353 | }
354 |
355 | // Fallback to in-memory QR code
356 | if (state.latestQrCode) {
357 | try {
358 | res.type('text/plain');
359 | return res.send(state.latestQrCode);
360 | } catch (error) {
361 | logger.error('[WA] Error sending QR code as text:', error);
362 | // Continue to final fallback
363 | }
364 | }
365 |
366 | // Final fallback - just return status
367 | if (state.whatsappError) {
368 | return res
369 | .status(500)
370 | .send(`WhatsApp initialization error: ${state.whatsappError.message}`);
371 | } else if (state.whatsappInitializing) {
372 | return res
373 | .status(202)
374 | .send('WhatsApp client is still initializing. Please try again in a minute.');
375 | } else if (state.clientReady) {
376 | return res.status(200).send('WhatsApp client is already authenticated. No QR code needed.');
377 | } else if (!state.whatsappInitStarted) {
378 | return res
379 | .status(200)
380 | .send('WhatsApp initialization has not been started yet. Check server logs.');
381 | } else {
382 | return res.status(404).send('QR code not yet available. Please try again in a moment.');
383 | }
384 | } catch (error) {
385 | logger.error('[WA] Unhandled error in QR endpoint:', error);
386 | res.status(500).send('Internal server error processing QR request');
387 | }
388 | });
389 |
390 | // Add status endpoint with enhanced error handling
391 | app.get('/status', (_req: Request, res: Response) => {
392 | try {
393 | res.status(200).json({
394 | server: 'running',
395 | uptime: state.environment.uptime(),
396 | startTime: serverStartTime.toISOString(),
397 | whatsappStarted: state.whatsappInitStarted,
398 | whatsapp: state.clientReady
399 | ? 'ready'
400 | : state.whatsappError
401 | ? 'error'
402 | : state.whatsappInitializing
403 | ? 'initializing'
404 | : 'not_started',
405 | error: state.whatsappError ? state.whatsappError.message : null,
406 | timestamp: new Date().toISOString(),
407 | });
408 | } catch (error) {
409 | logger.error('[WA] Error in status endpoint:', error);
410 | res.status(500).send('Error getting status');
411 | }
412 | });
413 |
414 | // Add memory usage endpoint for troubleshooting
415 | app.get('/memory-usage', (_req: Request, res: Response) => {
416 | try {
417 | const formatMemoryUsage = (data: number) =>
418 | `${Math.round((data / 1024 / 1024) * 100) / 100} MB`;
419 |
420 | const memoryData = process.memoryUsage();
421 |
422 | const memoryUsage = {
423 | rss: formatMemoryUsage(memoryData.rss),
424 | heapTotal: formatMemoryUsage(memoryData.heapTotal),
425 | heapUsed: formatMemoryUsage(memoryData.heapUsed),
426 | external: formatMemoryUsage(memoryData.external),
427 | arrayBuffers: formatMemoryUsage(memoryData.arrayBuffers || 0),
428 | rawData: memoryData,
429 | timestamp: new Date().toISOString(),
430 | };
431 |
432 | logger.info('[WA] Memory usage report:', memoryUsage);
433 | res.status(200).json(memoryUsage);
434 | } catch (error) {
435 | logger.error('[WA] Error in memory-usage endpoint:', error);
436 | res.status(500).send('Error getting memory usage');
437 | }
438 | });
439 |
440 | // API endpoint to get all chats (leverages the MCP get_chats tool)
441 | app.get('/api/chats', async (_req: Request, res: Response) => {
442 | try {
443 | if (!state.clientReady) {
444 | return res.status(503).json({
445 | status: 'error',
446 | message: 'WhatsApp client not ready',
447 | whatsappStatus: state.clientReady ? 'ready' : state.whatsappError ? 'error' : 'initializing',
448 | });
449 | }
450 |
451 | const whatsappClient = state.client;
452 | if (!whatsappClient) {
453 | return res.status(500).json({
454 | status: 'error',
455 | message: 'WhatsApp client not available',
456 | });
457 | }
458 |
459 | logger.info('[WA] Getting all chats');
460 | const chats = await whatsappClient.getChats();
461 |
462 | // Format the chats in a more API-friendly format
463 | const formattedChats = chats.map(chat => ({
464 | id: chat.id._serialized,
465 | name: chat.name,
466 | isGroup: chat.isGroup,
467 | timestamp: chat.timestamp ? new Date(chat.timestamp * 1000).toISOString() : null,
468 | unreadCount: chat.unreadCount,
469 | }));
470 |
471 | res.status(200).json({
472 | status: 'success',
473 | chats: formattedChats,
474 | });
475 | } catch (error) {
476 | logger.error('[WA] Error getting chats:', error);
477 | res.status(500).json({
478 | status: 'error',
479 | message: 'Failed to get chats',
480 | error: error instanceof Error ? error.message : String(error),
481 | });
482 | }
483 | });
484 |
485 | // API endpoint to get messages from a specific chat
486 | app.get('/api/chats/:chatId/messages', async (req: Request, res: Response) => {
487 | try {
488 | const { chatId } = req.params;
489 | const limit = req.query.limit ? parseInt(req.query.limit as string) : 50;
490 |
491 | if (!state.clientReady) {
492 | return res.status(503).json({
493 | status: 'error',
494 | message: 'WhatsApp client not ready',
495 | whatsappStatus: state.clientReady ? 'ready' : state.whatsappError ? 'error' : 'initializing',
496 | });
497 | }
498 |
499 | const whatsappClient = state.client;
500 | if (!whatsappClient) {
501 | return res.status(500).json({
502 | status: 'error',
503 | message: 'WhatsApp client not available',
504 | });
505 | }
506 |
507 | logger.info(`[WA] Getting messages for chat ${chatId} (limit: ${limit})`);
508 |
509 | // Get the chat by ID
510 | const chat = await whatsappClient.getChatById(chatId);
511 | if (!chat) {
512 | return res.status(404).json({
513 | status: 'error',
514 | message: `Chat with ID ${chatId} not found`,
515 | });
516 | }
517 |
518 | // Fetch messages
519 | const messages = await chat.fetchMessages({ limit });
520 |
521 | // Format messages in a more API-friendly format
522 | const formattedMessages = messages.map(msg => ({
523 | id: msg.id._serialized,
524 | body: msg.body,
525 | type: msg.type,
526 | timestamp: msg.timestamp ? new Date(msg.timestamp * 1000).toISOString() : null,
527 | from: msg.from,
528 | fromMe: msg.fromMe,
529 | hasMedia: msg.hasMedia,
530 | }));
531 |
532 | res.status(200).json({
533 | status: 'success',
534 | chatId: chatId,
535 | messages: formattedMessages,
536 | });
537 | } catch (error) {
538 | logger.error(`[WA] Error getting messages for chat:`, error);
539 | res.status(500).json({
540 | status: 'error',
541 | message: 'Failed to get messages',
542 | error: error instanceof Error ? error.message : String(error),
543 | });
544 | }
545 | });
546 |
547 | // ===================================================
548 | // REST API ENDPOINTS THAT MAP TO ALL MCP TOOLS
549 | // ===================================================
550 |
551 | // Utility function to check if WhatsApp client is ready
552 | const ensureClientReady = (res: Response) => {
553 | if (!state.clientReady) {
554 | res.status(503).json({
555 | status: 'error',
556 | message: 'WhatsApp client not ready',
557 | whatsappStatus: state.clientReady ? 'ready' : state.whatsappError ? 'error' : 'initializing',
558 | });
559 | return false;
560 | }
561 | return true;
562 | };
563 |
564 | // 1. GET STATUS ENDPOINT - Maps to get_status tool
565 | app.get('/api/status', (_req: Request, res: Response) => {
566 | try {
567 | const whatsappStatus = state.clientReady
568 | ? 'ready'
569 | : state.whatsappError
570 | ? 'error'
571 | : state.whatsappInitializing
572 | ? 'initializing'
573 | : 'not_started';
574 |
575 | res.status(200).json({
576 | status: 'success',
577 | whatsappStatus: whatsappStatus,
578 | uptime: state.environment.uptime(),
579 | startTime: serverStartTime.toISOString(),
580 | error: state.whatsappError ? state.whatsappError.message : null,
581 | });
582 | } catch (error) {
583 | logger.error('[WA] Error in status endpoint:', error);
584 | res.status(500).json({
585 | status: 'error',
586 | message: 'Failed to get status',
587 | error: error instanceof Error ? error.message : String(error),
588 | });
589 | }
590 | });
591 |
592 | // 2. SEARCH CONTACTS ENDPOINT - Maps to search_contacts tool
593 | app.get('/api/contacts/search', async (req: Request, res: Response) => {
594 | try {
595 | if (!ensureClientReady(res)) return;
596 |
597 | const query = req.query.query as string;
598 | if (!query) {
599 | return res.status(400).json({
600 | status: 'error',
601 | message: 'Missing query parameter',
602 | });
603 | }
604 |
605 | const whatsappClient = state.client;
606 | const contacts = await whatsappClient.getContacts();
607 | const filtered = contacts.filter(contact => {
608 | const name = contact.name || contact.pushname || '';
609 | const number = contact.number || contact.id?.user || '';
610 | return name.toLowerCase().includes(query.toLowerCase()) || number.includes(query);
611 | }).map(contact => ({
612 | id: contact.id._serialized,
613 | name: contact.name || contact.pushname || 'Unknown',
614 | number: contact.number || contact.id?.user || 'Unknown',
615 | type: contact.isGroup ? 'group' : 'individual',
616 | }));
617 |
618 | res.status(200).json({
619 | status: 'success',
620 | query: query,
621 | contacts: filtered,
622 | });
623 | } catch (error) {
624 | logger.error('[WA] Error searching contacts:', error);
625 | res.status(500).json({
626 | status: 'error',
627 | message: 'Failed to search contacts',
628 | error: error instanceof Error ? error.message : String(error),
629 | });
630 | }
631 | });
632 |
633 | // 3. GET MESSAGES ENDPOINT - Maps to get_messages tool
634 | app.get('/api/chats/:chatId/messages', async (req: Request, res: Response) => {
635 | try {
636 | if (!ensureClientReady(res)) return;
637 |
638 | const { chatId } = req.params;
639 | const limit = req.query.limit ? parseInt(req.query.limit as string) : 50;
640 |
641 | const whatsappClient = state.client;
642 |
643 | // Get the chat by ID
644 | const chat = await whatsappClient.getChatById(chatId);
645 | if (!chat) {
646 | return res.status(404).json({
647 | status: 'error',
648 | message: `Chat with ID ${chatId} not found`,
649 | });
650 | }
651 |
652 | // Fetch messages
653 | const messages = await chat.fetchMessages({ limit });
654 |
655 | // Format messages in a more API-friendly format
656 | const formattedMessages = messages.map(msg => ({
657 | id: msg.id._serialized,
658 | body: msg.body,
659 | type: msg.type,
660 | timestamp: msg.timestamp ? new Date(msg.timestamp * 1000).toISOString() : null,
661 | from: msg.from,
662 | fromMe: msg.fromMe,
663 | hasMedia: msg.hasMedia,
664 | }));
665 |
666 | res.status(200).json({
667 | status: 'success',
668 | chatId: chatId,
669 | messages: formattedMessages,
670 | });
671 | } catch (error) {
672 | logger.error(`[WA] Error getting messages for chat:`, error);
673 | res.status(500).json({
674 | status: 'error',
675 | message: 'Failed to get messages',
676 | error: error instanceof Error ? error.message : String(error),
677 | });
678 | }
679 | });
680 |
681 | // 4. GET CHATS ENDPOINT - Maps to get_chats tool
682 | app.get('/api/chats', async (_req: Request, res: Response) => {
683 | try {
684 | if (!ensureClientReady(res)) return;
685 |
686 | const whatsappClient = state.client;
687 | const chats = await whatsappClient.getChats();
688 |
689 | // Format the chats in a more API-friendly format
690 | const formattedChats = chats.map(chat => ({
691 | id: chat.id._serialized,
692 | name: chat.name,
693 | isGroup: chat.isGroup,
694 | timestamp: chat.timestamp ? new Date(chat.timestamp * 1000).toISOString() : null,
695 | unreadCount: chat.unreadCount,
696 | }));
697 |
698 | res.status(200).json({
699 | status: 'success',
700 | chats: formattedChats,
701 | });
702 | } catch (error) {
703 | logger.error('[WA] Error getting chats:', error);
704 | res.status(500).json({
705 | status: 'error',
706 | message: 'Failed to get chats',
707 | error: error instanceof Error ? error.message : String(error),
708 | });
709 | }
710 | });
711 |
712 | // 5. SEND MESSAGE ENDPOINT - Maps to send_message tool
713 | app.post('/api/chats/:chatId/messages', async (req: Request, res: Response) => {
714 | try {
715 | if (!ensureClientReady(res)) return;
716 |
717 | const { chatId } = req.params;
718 | const { message } = req.body;
719 |
720 | if (!message) {
721 | return res.status(400).json({
722 | status: 'error',
723 | message: 'Missing message in request body',
724 | });
725 | }
726 |
727 | const whatsappClient = state.client;
728 |
729 | // Get the chat by ID
730 | const chat = await whatsappClient.getChatById(chatId);
731 | if (!chat) {
732 | return res.status(404).json({
733 | status: 'error',
734 | message: `Chat with ID ${chatId} not found`,
735 | });
736 | }
737 |
738 | // Send the message
739 | const sentMessage = await chat.sendMessage(message);
740 |
741 | res.status(200).json({
742 | status: 'success',
743 | chatId: chatId,
744 | messageId: sentMessage.id._serialized,
745 | timestamp: new Date().toISOString(),
746 | });
747 | } catch (error) {
748 | logger.error(`[WA] Error sending message to chat:`, error);
749 | res.status(500).json({
750 | status: 'error',
751 | message: 'Failed to send message',
752 | error: error instanceof Error ? error.message : String(error),
753 | });
754 | }
755 | });
756 |
757 | // 6. GET GROUPS ENDPOINT - Maps to groups resource
758 | app.get('/api/groups', async (_req: Request, res: Response) => {
759 | try {
760 | if (!ensureClientReady(res)) return;
761 |
762 | const whatsappClient = state.client;
763 | const chats = await whatsappClient.getChats();
764 | const groups = chats.filter(chat => chat.isGroup).map(group => ({
765 | id: group.id._serialized,
766 | name: group.name,
767 | participants: group.participants?.map(p => ({
768 | id: p.id._serialized,
769 | isAdmin: p.isAdmin || false,
770 | })) || [],
771 | timestamp: group.timestamp ? new Date(group.timestamp * 1000).toISOString() : null,
772 | }));
773 |
774 | res.status(200).json({
775 | status: 'success',
776 | groups: groups,
777 | });
778 | } catch (error) {
779 | logger.error('[WA] Error getting groups:', error);
780 | res.status(500).json({
781 | status: 'error',
782 | message: 'Failed to get groups',
783 | error: error instanceof Error ? error.message : String(error),
784 | });
785 | }
786 | });
787 |
788 | // 7. SEARCH GROUPS ENDPOINT - Maps to search_groups resource
789 | app.get('/api/groups/search', async (req: Request, res: Response) => {
790 | try {
791 | if (!ensureClientReady(res)) return;
792 |
793 | const query = req.query.query as string;
794 | if (!query) {
795 | return res.status(400).json({
796 | status: 'error',
797 | message: 'Missing query parameter',
798 | });
799 | }
800 |
801 | const whatsappClient = state.client;
802 | const chats = await whatsappClient.getChats();
803 | const groups = chats.filter(chat => {
804 | return chat.isGroup && chat.name.toLowerCase().includes(query.toLowerCase());
805 | }).map(group => ({
806 | id: group.id._serialized,
807 | name: group.name,
808 | participants: group.participants?.length || 0,
809 | timestamp: group.timestamp ? new Date(group.timestamp * 1000).toISOString() : null,
810 | }));
811 |
812 | res.status(200).json({
813 | status: 'success',
814 | query: query,
815 | groups: groups,
816 | });
817 | } catch (error) {
818 | logger.error('[WA] Error searching groups:', error);
819 | res.status(500).json({
820 | status: 'error',
821 | message: 'Failed to search groups',
822 | error: error instanceof Error ? error.message : String(error),
823 | });
824 | }
825 | });
826 |
827 | // 8. GET GROUP MESSAGES ENDPOINT - Maps to group_messages resource
828 | app.get('/api/groups/:groupId/messages', async (req: Request, res: Response) => {
829 | try {
830 | if (!ensureClientReady(res)) return;
831 |
832 | const { groupId } = req.params;
833 | const limit = req.query.limit ? parseInt(req.query.limit as string) : 50;
834 |
835 | const whatsappClient = state.client;
836 |
837 | // Get the group chat by ID
838 | const chat = await whatsappClient.getChatById(groupId);
839 | if (!chat || !chat.isGroup) {
840 | return res.status(404).json({
841 | status: 'error',
842 | message: `Group with ID ${groupId} not found`,
843 | });
844 | }
845 |
846 | // Fetch messages
847 | const messages = await chat.fetchMessages({ limit });
848 |
849 | // Format messages
850 | const formattedMessages = messages.map(msg => ({
851 | id: msg.id._serialized,
852 | body: msg.body,
853 | type: msg.type,
854 | timestamp: msg.timestamp ? new Date(msg.timestamp * 1000).toISOString() : null,
855 | author: msg.author || msg.from,
856 | fromMe: msg.fromMe,
857 | hasMedia: msg.hasMedia,
858 | }));
859 |
860 | res.status(200).json({
861 | status: 'success',
862 | groupId: groupId,
863 | messages: formattedMessages,
864 | });
865 | } catch (error) {
866 | logger.error(`[WA] Error getting messages for group:`, error);
867 | res.status(500).json({
868 | status: 'error',
869 | message: 'Failed to get group messages',
870 | error: error instanceof Error ? error.message : String(error),
871 | });
872 | }
873 | });
874 |
875 | // 9. CREATE GROUP ENDPOINT - Maps to create_group tool
876 | app.post('/api/groups', async (req: Request, res: Response) => {
877 | try {
878 | if (!ensureClientReady(res)) return;
879 |
880 | const { name, participants } = req.body;
881 |
882 | if (!name || !participants || !Array.isArray(participants)) {
883 | return res.status(400).json({
884 | status: 'error',
885 | message: 'Missing name or participants array in request body',
886 | });
887 | }
888 |
889 | const whatsappClient = state.client;
890 | const result = await whatsappClient.createGroup(name, participants);
891 |
892 | res.status(200).json({
893 | status: 'success',
894 | group: {
895 | id: result.gid._serialized,
896 | name: name,
897 | participants: participants,
898 | },
899 | });
900 | } catch (error) {
901 | logger.error('[WA] Error creating group:', error);
902 | res.status(500).json({
903 | status: 'error',
904 | message: 'Failed to create group',
905 | error: error instanceof Error ? error.message : String(error),
906 | });
907 | }
908 | });
909 |
910 | // 10. ADD PARTICIPANTS TO GROUP ENDPOINT - Maps to add_participants_to_group tool
911 | app.post('/api/groups/:groupId/participants', async (req: Request, res: Response) => {
912 | try {
913 | if (!ensureClientReady(res)) return;
914 |
915 | const { groupId } = req.params;
916 | const { participants } = req.body;
917 |
918 | if (!participants || !Array.isArray(participants)) {
919 | return res.status(400).json({
920 | status: 'error',
921 | message: 'Missing participants array in request body',
922 | });
923 | }
924 |
925 | const whatsappClient = state.client;
926 |
927 | // Get the group chat by ID
928 | const chat = await whatsappClient.getChatById(groupId);
929 | if (!chat || !chat.isGroup) {
930 | return res.status(404).json({
931 | status: 'error',
932 | message: `Group with ID ${groupId} not found`,
933 | });
934 | }
935 |
936 | // Add participants
937 | const result = await chat.addParticipants(participants);
938 |
939 | res.status(200).json({
940 | status: 'success',
941 | groupId: groupId,
942 | added: result,
943 | });
944 | } catch (error) {
945 | logger.error(`[WA] Error adding participants to group:`, error);
946 | res.status(500).json({
947 | status: 'error',
948 | message: 'Failed to add participants to group',
949 | error: error instanceof Error ? error.message : String(error),
950 | });
951 | }
952 | });
953 |
954 | // Add environment variables endpoint for troubleshooting
955 | app.get('/container-env', (_req: Request, res: Response) => {
956 | try {
957 | // Don't log or expose sensitive values
958 | const sanitizedEnv = Object.fromEntries(
959 | Object.entries(process.env)
960 | .filter(
961 | ([key]) =>
962 | !key.toLowerCase().includes('key') &&
963 | !key.toLowerCase().includes('token') &&
964 | !key.toLowerCase().includes('secret') &&
965 | !key.toLowerCase().includes('pass') &&
966 | !key.toLowerCase().includes('auth'),
967 | )
968 | .map(([key, value]) => [key, value]),
969 | );
970 |
971 | const envData = {
972 | nodeVersion: process.version,
973 | platform: process.platform,
974 | arch: process.arch,
975 | containerVars: {
976 | PORT: process.env.PORT,
977 | NODE_ENV: process.env.NODE_ENV,
978 | DOCKER_CONTAINER: process.env.DOCKER_CONTAINER,
979 | RENDER: process.env.RENDER,
980 | },
981 | // Include sanitized env for debugging only
982 | fullEnv: sanitizedEnv,
983 | timestamp: new Date().toISOString(),
984 | };
985 |
986 | logger.info('[WA] Container environment report');
987 | res.status(200).json(envData);
988 | } catch (error) {
989 | logger.error('[WA] Error in container-env endpoint:', error);
990 | res.status(500).send('Error getting container environment');
991 | }
992 | });
993 |
994 | // Add file system exploration endpoint for troubleshooting
995 | app.get('/filesys', (_req: Request, res: Response) => {
996 | try {
997 | const directoriesToCheck = [
998 | '/',
999 | '/app',
1000 | '/app/data',
1001 | '/app/data/whatsapp',
1002 | '/var',
1003 | '/var/data',
1004 | '/var/data/whatsapp',
1005 | '/tmp',
1006 | '/tmp/puppeteer_data',
1007 | ];
1008 |
1009 | const fsData = directoriesToCheck.map(dir => {
1010 | try {
1011 | const exists = fs.existsSync(dir);
1012 | let files: string[] = [];
1013 | let stats = null;
1014 |
1015 | if (exists) {
1016 | try {
1017 | stats = fs.statSync(dir);
1018 | files = fs.readdirSync(dir).slice(0, 20); // Only get first 20 files
1019 | } catch (e) {
1020 | files = [`Error reading directory: ${e instanceof Error ? e.message : String(e)}`];
1021 | }
1022 | }
1023 |
1024 | return {
1025 | directory: dir,
1026 | exists,
1027 | stats: stats
1028 | ? {
1029 | isDirectory: stats.isDirectory(),
1030 | size: stats.size,
1031 | mode: stats.mode,
1032 | uid: stats.uid,
1033 | gid: stats.gid,
1034 | }
1035 | : null,
1036 | files,
1037 | };
1038 | } catch (e) {
1039 | return {
1040 | directory: dir,
1041 | error: e instanceof Error ? e.message : String(e),
1042 | };
1043 | }
1044 | });
1045 |
1046 | logger.info('[WA] File system exploration report');
1047 | res.status(200).json(fsData);
1048 | } catch (error) {
1049 | logger.error('[WA] Error in filesys endpoint:', error);
1050 | res.status(500).send('Error exploring file system');
1051 | }
1052 | });
1053 |
1054 | // Add start WhatsApp endpoint - separated from server start
1055 | app.get('/start-whatsapp', (_req: Request, res: Response) => {
1056 | // Only start once
1057 | if (state.whatsappInitStarted) {
1058 | return res.status(200).json({
1059 | status: 'WhatsApp initialization already started',
1060 | clientReady: state.clientReady,
1061 | error: state.whatsappError ? state.whatsappError.message : null,
1062 | });
1063 | }
1064 |
1065 | // Start WhatsApp initialization
1066 | state.whatsappInitStarted = true;
1067 | state.whatsappInitializing = true;
1068 |
1069 | // Launch initialization in the background
1070 | initializeWhatsAppClient(whatsAppConfig, state);
1071 |
1072 | return res.status(200).json({
1073 | status: 'WhatsApp initialization started',
1074 | message: 'Check /status for updates',
1075 | });
1076 | });
1077 |
1078 | // Start server IMMEDIATELY - BEFORE client initialization
1079 | // This is CRITICAL to prevent Render deployment failures
1080 | const serverPort = port || parseInt(process.env.PORT || '') || 3000;
1081 | logger.info(`[WA] Starting HTTP server on port ${serverPort}`);
1082 |
1083 | const server = app.listen(serverPort, '0.0.0.0', () => {
1084 | logger.info(`[WA] WhatsApp Web Client API server started on port ${serverPort}`);
1085 | });
1086 |
1087 | // Set additional error handlers for process
1088 | process.on('uncaughtException', error => {
1089 | logger.error('[WA] Uncaught exception:', error);
1090 | // Don't crash the server
1091 | });
1092 |
1093 | process.on('unhandledRejection', reason => {
1094 | logger.error('[WA] Unhandled rejection:', reason);
1095 | // Don't crash the server
1096 | });
1097 |
1098 | // Keep the process running
1099 | process.on('SIGINT', async () => {
1100 | logger.info('[WA] Shutting down WhatsApp Web Client API...');
1101 | server.close();
1102 | process.exit(0);
1103 | });
1104 | }
1105 |
1106 | // Separate function to initialize WhatsApp client
1107 | async function initializeWhatsAppClient(whatsAppConfig: WhatsAppConfig, state: any): Promise<void> {
1108 | let client: Client | null = null;
1109 |
1110 | try {
1111 | logger.info('[WA] Starting WhatsApp client initialization...');
1112 |
1113 | // Create the client
1114 | client = createWhatsAppClient(whatsAppConfig);
1115 |
1116 | // Capture the QR code
1117 | client.on('qr', qr => {
1118 | logger.info('[WA] New QR code received');
1119 | state.latestQrCode = qr;
1120 | // QR code file saving is handled in whatsapp-client.ts
1121 |
1122 | // Also log QR code to console for terminal access
1123 | try {
1124 | // Use a smaller QR code with proper formatting
1125 | logger.info('[WA] Scan this QR code with your WhatsApp app:');
1126 | const qrcodeTerminal = require('qrcode-terminal');
1127 | qrcodeTerminal.generate(qr, { small: true }, function (qrcode: string) {
1128 | // Split the QR code by lines and log each line separately to preserve formatting
1129 | const qrLines = qrcode.split('\n');
1130 | qrLines.forEach((line: string) => {
1131 | logger.info(`[WA-QR] ${line}`);
1132 | });
1133 | });
1134 | } catch (error) {
1135 | logger.error('[WA] Failed to generate terminal QR code', error);
1136 | }
1137 | });
1138 |
1139 | client.on('ready', () => {
1140 | state.clientReady = true;
1141 | state.whatsappInitializing = false;
1142 | logger.info('[WA] Client is ready');
1143 | });
1144 |
1145 | client.on('auth_failure', error => {
1146 | state.whatsappError = new Error(`Authentication failed: ${error}`);
1147 | logger.error('[WA] Authentication failed:', error);
1148 | });
1149 |
1150 | client.on('disconnected', reason => {
1151 | logger.warn('[WA] Client disconnected:', reason);
1152 | state.clientReady = false;
1153 | });
1154 |
1155 | await client.initialize();
1156 | } catch (error) {
1157 | state.whatsappInitializing = false;
1158 | state.whatsappError = error as Error;
1159 | logger.error('[WA] Error during client initialization:', error);
1160 | // Don't throw here - we want the server to keep running even if WhatsApp fails
1161 | }
1162 | }
1163 |
1164 | async function startMcpServer(
1165 | mcpConfig: McpConfig,
1166 | transport: string,
1167 | port: number,
1168 | mode: string,
1169 | ): Promise<void> {
1170 | let client: Client | null = null;
1171 | if (mode === 'standalone') {
1172 | logger.info('Starting WhatsApp Web Client...');
1173 | client = createWhatsAppClient(mcpConfig.whatsappConfig);
1174 | await client.initialize();
1175 | }
1176 |
1177 | logger.info(`Starting MCP server in ${mode} mode...`);
1178 | logger.debug('MCP Configuration:', mcpConfig);
1179 |
1180 | const server = createMcpServer(mcpConfig, client);
1181 |
1182 | if (transport === 'sse') {
1183 | await startMcpSseServer(server, port, mode);
1184 | } else if (transport === 'command') {
1185 | await startMcpCommandServer(server, mode);
1186 | }
1187 | }
1188 |
1189 | async function main(): Promise<void> {
1190 | try {
1191 | const argv = parseCommandLineArgs();
1192 | configureLogger(argv);
1193 |
1194 | const { whatsAppConfig, mcpConfig } = createConfigurations(argv);
1195 |
1196 | if (argv.mode === 'mcp') {
1197 | await startMcpServer(
1198 | mcpConfig,
1199 | argv['transport'] as string,
1200 | argv['sse-port'] as number,
1201 | argv['mcp-mode'] as string,
1202 | );
1203 | } else if (argv.mode === 'whatsapp-api') {
1204 | await startWhatsAppApiServer(whatsAppConfig, argv['api-port'] as number);
1205 | }
1206 | } catch (error) {
1207 | logger.error('Error starting application:', error);
1208 | process.exit(1);
1209 | }
1210 | }
1211 |
1212 | main();
1213 |
```