This is page 2 of 2. Use http://codebase.md/fyimail/whatsapp-mcp2?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/main.ts:
--------------------------------------------------------------------------------
```typescript
import express, { NextFunction, Request, Response } from 'express';
import { createMcpServer, McpConfig } from './mcp-server';
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { createWhatsAppClient, WhatsAppConfig } from './whatsapp-client';
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';
import logger, { configureForCommandMode } from './logger';
import { requestLogger, errorHandler } from './middleware';
import { routerFactory } from './api';
import { Client } from 'whatsapp-web.js';
import fs from 'fs';
import path from 'path';
import crypto from 'crypto';
const isDockerContainer = process.env.DOCKER_CONTAINER === 'true';
function parseCommandLineArgs(): ReturnType<typeof yargs.parseSync> {
return yargs(hideBin(process.argv))
.option('mode', {
alias: 'm',
description: 'Run mode: mcp or whatsapp-api',
type: 'string',
choices: ['mcp', 'whatsapp-api'],
default: 'mcp',
})
.option('mcp-mode', {
alias: 'c',
description:
'MCP connection mode: standalone (direct WhatsApp client) or api (connect to WhatsApp API)',
type: 'string',
choices: ['standalone', 'api'],
default: 'standalone',
})
.option('transport', {
alias: 't',
description: 'MCP transport mode: sse or command',
type: 'string',
choices: ['sse', 'command'],
default: 'sse',
})
.option('sse-port', {
alias: 'p',
description: 'Port for SSE server',
type: 'number',
default: 3002,
})
.option('api-port', {
description: 'Port for WhatsApp API server',
type: 'number',
default: 3002,
})
.option('auth-data-path', {
alias: 'a',
description: 'Path to store authentication data',
type: 'string',
default: '.wwebjs_auth',
})
.option('auth-strategy', {
alias: 's',
description: 'Authentication strategy: local or none',
type: 'string',
choices: ['local', 'none'],
default: 'local',
})
.option('api-key', {
alias: 'k',
description: 'API key for WhatsApp Web REST API when using api mode',
type: 'string',
default: '',
})
.option('log-level', {
alias: 'l',
description: 'Log level: error, warn, info, http, debug',
type: 'string',
choices: ['error', 'warn', 'info', 'http', 'debug'],
default: 'info',
})
.help()
.alias('help', 'h')
.parseSync();
}
function configureLogger(argv: ReturnType<typeof parseCommandLineArgs>): void {
logger.level = argv['log-level'] as string;
// Configure logger to use stderr for all levels when in MCP command mode
if (argv.mode === 'mcp' && argv.transport === 'command') {
configureForCommandMode();
}
}
function createConfigurations(argv: ReturnType<typeof parseCommandLineArgs>): {
whatsAppConfig: WhatsAppConfig;
mcpConfig: McpConfig;
} {
const whatsAppConfig: WhatsAppConfig = {
authDir: argv['auth-data-path'] as string,
authStrategy: argv['auth-strategy'] as 'local' | 'none',
dockerContainer: isDockerContainer,
};
const mcpConfig: McpConfig = {
useApiClient: argv['mcp-mode'] === 'api',
apiKey: argv['api-key'] as string,
whatsappConfig: whatsAppConfig,
};
return { whatsAppConfig, mcpConfig };
}
async function startMcpSseServer(
server: ReturnType<typeof createMcpServer>,
port: number,
mode: string,
): Promise<void> {
const app = express();
app.use(requestLogger);
let transport: SSEServerTransport;
app.get('/sse', async (_req: Request, res: Response) => {
logger.info('Received SSE connection');
transport = new SSEServerTransport('/message', res);
await server.connect(transport);
});
app.post('/message', async (req: Request, res: Response) => {
await transport?.handlePostMessage(req, res);
});
app.use(errorHandler);
app.listen(port, '0.0.0.0', () => {
logger.info(`MCP server is running on port ${port} in ${mode} mode`);
});
}
async function startMcpCommandServer(
server: ReturnType<typeof createMcpServer>,
mode: string,
): Promise<void> {
try {
const transport = new StdioServerTransport();
await server.connect(transport);
logger.info(`WhatsApp MCP server started successfully in ${mode} mode`);
process.stdin.on('close', () => {
logger.info('WhatsApp MCP Server closed');
server.close();
});
} catch (error) {
logger.error('Error connecting to MCP server', error);
}
}
async function getWhatsAppApiKey(whatsAppConfig: WhatsAppConfig): Promise<string> {
if (whatsAppConfig.authStrategy === 'none') {
return crypto.randomBytes(32).toString('hex');
}
const authDataPath = whatsAppConfig.authDir;
if (!authDataPath) {
throw new Error('The auth-data-path is required when using whatsapp-api mode');
}
const apiKeyPath = path.join(authDataPath, 'api_key.txt');
if (!fs.existsSync(apiKeyPath)) {
const apiKey = crypto.randomBytes(32).toString('hex');
fs.writeFileSync(apiKeyPath, apiKey);
return apiKey;
}
return fs.readFileSync(apiKeyPath, 'utf8');
}
async function startWhatsAppApiServer(whatsAppConfig: WhatsAppConfig, port: number): Promise<void> {
logger.info('[WA] Starting WhatsApp Web REST API...');
// Create the Express app before initializing WhatsApp client
const app = express();
// Add error handling to all middleware
app.use((req: Request, res: Response, next: NextFunction) => {
try {
requestLogger(req, res, next);
} catch (error) {
logger.error('[WA] Error in request logger middleware:', error);
next();
}
});
app.use(express.json());
// CRITICAL: Track server start time - helps with troubleshooting
const serverStartTime = new Date();
// Set up minimal state management for diagnostics
const state = {
whatsappInitializing: false,
whatsappInitStarted: false,
whatsappError: null as Error | null,
clientReady: false,
latestQrCode: null as string | null,
client: null as any, // Will hold the WhatsApp client instance once initialized
environment: {
node: process.version,
platform: process.platform,
port: port || process.env.PORT || 3000,
pid: process.pid,
uptime: () => Math.floor((new Date().getTime() - serverStartTime.getTime()) / 1000),
},
};
// Log important startup information
logger.info(
`[WA] Server starting with Node ${state.environment.node} on ${state.environment.platform}`,
);
logger.info(`[WA] Process ID: ${state.environment.pid}`);
logger.info(`[WA] Port: ${state.environment.port}`);
logger.info(`[WA] Start time: ${serverStartTime.toISOString()}`);
// EMERGENCY DIAGNOSTIC endpoint - absolutely minimal, will help diagnose deployment issues
app.get('/', (_req: Request, res: Response) => {
res.status(200).send(`
<html>
<head><title>WhatsApp API Service</title></head>
<body>
<h1>WhatsApp API Service</h1>
<p>Server is running</p>
<p>Uptime: ${state.environment.uptime()} seconds</p>
<p>Started: ${serverStartTime.toISOString()}</p>
<p>Node: ${state.environment.node}</p>
<p>Platform: ${state.environment.platform}</p>
<p>WhatsApp Status: ${
state.whatsappInitStarted
? state.clientReady
? 'Ready'
: state.whatsappError
? 'Error'
: 'Initializing'
: 'Not started'
}</p>
<ul>
<li><a href="/health">Health Check</a></li>
<li><a href="/memory-usage">Memory Usage</a></li>
<li><a href="/container-env">Container Environment</a></li>
<li><a href="/filesys">File System Check</a></li>
<li><a href="/qr">QR Code</a> (if available)</li>
</ul>
</body>
</html>
`);
});
// Add health check endpoint that doesn't require authentication
// CRITICAL: This must be minimal and not depend on any WhatsApp state
app.get('/health', (_req: Request, res: Response) => {
try {
// Always return 200 for Render health check, even if WhatsApp is still initializing
res.status(200).json({
status: 'ok',
server: 'running',
uptime: state.environment.uptime(),
startTime: serverStartTime.toISOString(),
whatsappStarted: state.whatsappInitStarted,
whatsapp: state.clientReady
? 'ready'
: state.whatsappError
? 'error'
: state.whatsappInitializing
? 'initializing'
: 'not_started',
timestamp: new Date().toISOString(),
});
} catch (error) {
logger.error('[WA] Error in health check endpoint:', error);
// Still return 200 to keep Render happy
res.status(200).send('OK');
}
});
// Add /wa-api endpoint for backwards compatibility with previous implementation
app.get('/wa-api', (_req: Request, res: Response) => {
try {
// Get the API key from the same place as the official implementation
const apiKeyPath = path.join(whatsAppConfig.authDir || '.wwebjs_auth', 'api_key.txt');
if (fs.existsSync(apiKeyPath)) {
const apiKey = fs.readFileSync(apiKeyPath, 'utf8');
logger.info('[WA] API key retrieved for /wa-api endpoint');
res.status(200).json({
status: 'success',
message: 'WhatsApp API key',
apiKey: apiKey,
});
} else {
logger.warn('[WA] API key file not found for /wa-api endpoint');
res.status(404).json({
status: 'error',
message: 'API key not found. Service might still be initializing.',
});
}
} catch (error) {
logger.error('[WA] Error retrieving API key for /wa-api endpoint:', error);
res.status(500).json({
status: 'error',
message: 'Failed to retrieve API key',
error: error instanceof Error ? error.message : String(error),
});
}
});
// Add QR code endpoint with enhanced error handling
app.get('/qr', (_req: Request, res: Response) => {
try {
// First try to get QR from file
try {
const qrPath = path.join('/var/data/whatsapp', 'last-qr.txt');
if (fs.existsSync(qrPath)) {
try {
const qrCode = fs.readFileSync(qrPath, 'utf8');
return res.send(`
<html>
<head>
<title>WhatsApp QR Code</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body { font-family: Arial, sans-serif; text-align: center; padding: 20px; }
.qr-container { margin: 20px auto; }
pre { background: #f4f4f4; padding: 20px; display: inline-block; text-align: left; }
.status { color: #555; margin: 20px 0; }
</style>
</head>
<body>
<h1>WhatsApp QR Code</h1>
<p>Scan this QR code with your WhatsApp app to link your device</p>
<div class="qr-container">
<pre>${qrCode}</pre>
</div>
<p class="status">Server status: ${state.whatsappInitializing ? 'Initializing WhatsApp...' : state.clientReady ? 'WhatsApp Ready' : 'Waiting for authentication'}</p>
<p><small>Last updated: ${new Date().toISOString()}</small></p>
<p><a href="/">Back to Home</a></p>
</body>
</html>
`);
} catch (readError) {
logger.error('[WA] Error reading QR file:', readError);
// Continue to fallback methods
}
}
} catch (fileError) {
logger.error('[WA] Error accessing QR file system:', fileError);
// Continue to fallback methods
}
// Fallback to in-memory QR code
if (state.latestQrCode) {
try {
res.type('text/plain');
return res.send(state.latestQrCode);
} catch (error) {
logger.error('[WA] Error sending QR code as text:', error);
// Continue to final fallback
}
}
// Final fallback - just return status
if (state.whatsappError) {
return res
.status(500)
.send(`WhatsApp initialization error: ${state.whatsappError.message}`);
} else if (state.whatsappInitializing) {
return res
.status(202)
.send('WhatsApp client is still initializing. Please try again in a minute.');
} else if (state.clientReady) {
return res.status(200).send('WhatsApp client is already authenticated. No QR code needed.');
} else if (!state.whatsappInitStarted) {
return res
.status(200)
.send('WhatsApp initialization has not been started yet. Check server logs.');
} else {
return res.status(404).send('QR code not yet available. Please try again in a moment.');
}
} catch (error) {
logger.error('[WA] Unhandled error in QR endpoint:', error);
res.status(500).send('Internal server error processing QR request');
}
});
// Add status endpoint with enhanced error handling
app.get('/status', (_req: Request, res: Response) => {
try {
res.status(200).json({
server: 'running',
uptime: state.environment.uptime(),
startTime: serverStartTime.toISOString(),
whatsappStarted: state.whatsappInitStarted,
whatsapp: state.clientReady
? 'ready'
: state.whatsappError
? 'error'
: state.whatsappInitializing
? 'initializing'
: 'not_started',
error: state.whatsappError ? state.whatsappError.message : null,
timestamp: new Date().toISOString(),
});
} catch (error) {
logger.error('[WA] Error in status endpoint:', error);
res.status(500).send('Error getting status');
}
});
// Add memory usage endpoint for troubleshooting
app.get('/memory-usage', (_req: Request, res: Response) => {
try {
const formatMemoryUsage = (data: number) =>
`${Math.round((data / 1024 / 1024) * 100) / 100} MB`;
const memoryData = process.memoryUsage();
const memoryUsage = {
rss: formatMemoryUsage(memoryData.rss),
heapTotal: formatMemoryUsage(memoryData.heapTotal),
heapUsed: formatMemoryUsage(memoryData.heapUsed),
external: formatMemoryUsage(memoryData.external),
arrayBuffers: formatMemoryUsage(memoryData.arrayBuffers || 0),
rawData: memoryData,
timestamp: new Date().toISOString(),
};
logger.info('[WA] Memory usage report:', memoryUsage);
res.status(200).json(memoryUsage);
} catch (error) {
logger.error('[WA] Error in memory-usage endpoint:', error);
res.status(500).send('Error getting memory usage');
}
});
// API endpoint to get all chats (leverages the MCP get_chats tool)
app.get('/api/chats', async (_req: Request, res: Response) => {
try {
if (!state.clientReady) {
return res.status(503).json({
status: 'error',
message: 'WhatsApp client not ready',
whatsappStatus: state.clientReady ? 'ready' : state.whatsappError ? 'error' : 'initializing',
});
}
const whatsappClient = state.client;
if (!whatsappClient) {
return res.status(500).json({
status: 'error',
message: 'WhatsApp client not available',
});
}
logger.info('[WA] Getting all chats');
const chats = await whatsappClient.getChats();
// Format the chats in a more API-friendly format
const formattedChats = chats.map(chat => ({
id: chat.id._serialized,
name: chat.name,
isGroup: chat.isGroup,
timestamp: chat.timestamp ? new Date(chat.timestamp * 1000).toISOString() : null,
unreadCount: chat.unreadCount,
}));
res.status(200).json({
status: 'success',
chats: formattedChats,
});
} catch (error) {
logger.error('[WA] Error getting chats:', error);
res.status(500).json({
status: 'error',
message: 'Failed to get chats',
error: error instanceof Error ? error.message : String(error),
});
}
});
// API endpoint to get messages from a specific chat
app.get('/api/chats/:chatId/messages', async (req: Request, res: Response) => {
try {
const { chatId } = req.params;
const limit = req.query.limit ? parseInt(req.query.limit as string) : 50;
if (!state.clientReady) {
return res.status(503).json({
status: 'error',
message: 'WhatsApp client not ready',
whatsappStatus: state.clientReady ? 'ready' : state.whatsappError ? 'error' : 'initializing',
});
}
const whatsappClient = state.client;
if (!whatsappClient) {
return res.status(500).json({
status: 'error',
message: 'WhatsApp client not available',
});
}
logger.info(`[WA] Getting messages for chat ${chatId} (limit: ${limit})`);
// Get the chat by ID
const chat = await whatsappClient.getChatById(chatId);
if (!chat) {
return res.status(404).json({
status: 'error',
message: `Chat with ID ${chatId} not found`,
});
}
// Fetch messages
const messages = await chat.fetchMessages({ limit });
// Format messages in a more API-friendly format
const formattedMessages = messages.map(msg => ({
id: msg.id._serialized,
body: msg.body,
type: msg.type,
timestamp: msg.timestamp ? new Date(msg.timestamp * 1000).toISOString() : null,
from: msg.from,
fromMe: msg.fromMe,
hasMedia: msg.hasMedia,
}));
res.status(200).json({
status: 'success',
chatId: chatId,
messages: formattedMessages,
});
} catch (error) {
logger.error(`[WA] Error getting messages for chat:`, error);
res.status(500).json({
status: 'error',
message: 'Failed to get messages',
error: error instanceof Error ? error.message : String(error),
});
}
});
// ===================================================
// REST API ENDPOINTS THAT MAP TO ALL MCP TOOLS
// ===================================================
// Utility function to check if WhatsApp client is ready
const ensureClientReady = (res: Response) => {
if (!state.clientReady) {
res.status(503).json({
status: 'error',
message: 'WhatsApp client not ready',
whatsappStatus: state.clientReady ? 'ready' : state.whatsappError ? 'error' : 'initializing',
});
return false;
}
return true;
};
// 1. GET STATUS ENDPOINT - Maps to get_status tool
app.get('/api/status', (_req: Request, res: Response) => {
try {
const whatsappStatus = state.clientReady
? 'ready'
: state.whatsappError
? 'error'
: state.whatsappInitializing
? 'initializing'
: 'not_started';
res.status(200).json({
status: 'success',
whatsappStatus: whatsappStatus,
uptime: state.environment.uptime(),
startTime: serverStartTime.toISOString(),
error: state.whatsappError ? state.whatsappError.message : null,
});
} catch (error) {
logger.error('[WA] Error in status endpoint:', error);
res.status(500).json({
status: 'error',
message: 'Failed to get status',
error: error instanceof Error ? error.message : String(error),
});
}
});
// 2. SEARCH CONTACTS ENDPOINT - Maps to search_contacts tool
app.get('/api/contacts/search', async (req: Request, res: Response) => {
try {
if (!ensureClientReady(res)) return;
const query = req.query.query as string;
if (!query) {
return res.status(400).json({
status: 'error',
message: 'Missing query parameter',
});
}
const whatsappClient = state.client;
const contacts = await whatsappClient.getContacts();
const filtered = contacts.filter(contact => {
const name = contact.name || contact.pushname || '';
const number = contact.number || contact.id?.user || '';
return name.toLowerCase().includes(query.toLowerCase()) || number.includes(query);
}).map(contact => ({
id: contact.id._serialized,
name: contact.name || contact.pushname || 'Unknown',
number: contact.number || contact.id?.user || 'Unknown',
type: contact.isGroup ? 'group' : 'individual',
}));
res.status(200).json({
status: 'success',
query: query,
contacts: filtered,
});
} catch (error) {
logger.error('[WA] Error searching contacts:', error);
res.status(500).json({
status: 'error',
message: 'Failed to search contacts',
error: error instanceof Error ? error.message : String(error),
});
}
});
// 3. GET MESSAGES ENDPOINT - Maps to get_messages tool
app.get('/api/chats/:chatId/messages', async (req: Request, res: Response) => {
try {
if (!ensureClientReady(res)) return;
const { chatId } = req.params;
const limit = req.query.limit ? parseInt(req.query.limit as string) : 50;
const whatsappClient = state.client;
// Get the chat by ID
const chat = await whatsappClient.getChatById(chatId);
if (!chat) {
return res.status(404).json({
status: 'error',
message: `Chat with ID ${chatId} not found`,
});
}
// Fetch messages
const messages = await chat.fetchMessages({ limit });
// Format messages in a more API-friendly format
const formattedMessages = messages.map(msg => ({
id: msg.id._serialized,
body: msg.body,
type: msg.type,
timestamp: msg.timestamp ? new Date(msg.timestamp * 1000).toISOString() : null,
from: msg.from,
fromMe: msg.fromMe,
hasMedia: msg.hasMedia,
}));
res.status(200).json({
status: 'success',
chatId: chatId,
messages: formattedMessages,
});
} catch (error) {
logger.error(`[WA] Error getting messages for chat:`, error);
res.status(500).json({
status: 'error',
message: 'Failed to get messages',
error: error instanceof Error ? error.message : String(error),
});
}
});
// 4. GET CHATS ENDPOINT - Maps to get_chats tool
app.get('/api/chats', async (_req: Request, res: Response) => {
try {
if (!ensureClientReady(res)) return;
const whatsappClient = state.client;
const chats = await whatsappClient.getChats();
// Format the chats in a more API-friendly format
const formattedChats = chats.map(chat => ({
id: chat.id._serialized,
name: chat.name,
isGroup: chat.isGroup,
timestamp: chat.timestamp ? new Date(chat.timestamp * 1000).toISOString() : null,
unreadCount: chat.unreadCount,
}));
res.status(200).json({
status: 'success',
chats: formattedChats,
});
} catch (error) {
logger.error('[WA] Error getting chats:', error);
res.status(500).json({
status: 'error',
message: 'Failed to get chats',
error: error instanceof Error ? error.message : String(error),
});
}
});
// 5. SEND MESSAGE ENDPOINT - Maps to send_message tool
app.post('/api/chats/:chatId/messages', async (req: Request, res: Response) => {
try {
if (!ensureClientReady(res)) return;
const { chatId } = req.params;
const { message } = req.body;
if (!message) {
return res.status(400).json({
status: 'error',
message: 'Missing message in request body',
});
}
const whatsappClient = state.client;
// Get the chat by ID
const chat = await whatsappClient.getChatById(chatId);
if (!chat) {
return res.status(404).json({
status: 'error',
message: `Chat with ID ${chatId} not found`,
});
}
// Send the message
const sentMessage = await chat.sendMessage(message);
res.status(200).json({
status: 'success',
chatId: chatId,
messageId: sentMessage.id._serialized,
timestamp: new Date().toISOString(),
});
} catch (error) {
logger.error(`[WA] Error sending message to chat:`, error);
res.status(500).json({
status: 'error',
message: 'Failed to send message',
error: error instanceof Error ? error.message : String(error),
});
}
});
// 6. GET GROUPS ENDPOINT - Maps to groups resource
app.get('/api/groups', async (_req: Request, res: Response) => {
try {
if (!ensureClientReady(res)) return;
const whatsappClient = state.client;
const chats = await whatsappClient.getChats();
const groups = chats.filter(chat => chat.isGroup).map(group => ({
id: group.id._serialized,
name: group.name,
participants: group.participants?.map(p => ({
id: p.id._serialized,
isAdmin: p.isAdmin || false,
})) || [],
timestamp: group.timestamp ? new Date(group.timestamp * 1000).toISOString() : null,
}));
res.status(200).json({
status: 'success',
groups: groups,
});
} catch (error) {
logger.error('[WA] Error getting groups:', error);
res.status(500).json({
status: 'error',
message: 'Failed to get groups',
error: error instanceof Error ? error.message : String(error),
});
}
});
// 7. SEARCH GROUPS ENDPOINT - Maps to search_groups resource
app.get('/api/groups/search', async (req: Request, res: Response) => {
try {
if (!ensureClientReady(res)) return;
const query = req.query.query as string;
if (!query) {
return res.status(400).json({
status: 'error',
message: 'Missing query parameter',
});
}
const whatsappClient = state.client;
const chats = await whatsappClient.getChats();
const groups = chats.filter(chat => {
return chat.isGroup && chat.name.toLowerCase().includes(query.toLowerCase());
}).map(group => ({
id: group.id._serialized,
name: group.name,
participants: group.participants?.length || 0,
timestamp: group.timestamp ? new Date(group.timestamp * 1000).toISOString() : null,
}));
res.status(200).json({
status: 'success',
query: query,
groups: groups,
});
} catch (error) {
logger.error('[WA] Error searching groups:', error);
res.status(500).json({
status: 'error',
message: 'Failed to search groups',
error: error instanceof Error ? error.message : String(error),
});
}
});
// 8. GET GROUP MESSAGES ENDPOINT - Maps to group_messages resource
app.get('/api/groups/:groupId/messages', async (req: Request, res: Response) => {
try {
if (!ensureClientReady(res)) return;
const { groupId } = req.params;
const limit = req.query.limit ? parseInt(req.query.limit as string) : 50;
const whatsappClient = state.client;
// Get the group chat by ID
const chat = await whatsappClient.getChatById(groupId);
if (!chat || !chat.isGroup) {
return res.status(404).json({
status: 'error',
message: `Group with ID ${groupId} not found`,
});
}
// Fetch messages
const messages = await chat.fetchMessages({ limit });
// Format messages
const formattedMessages = messages.map(msg => ({
id: msg.id._serialized,
body: msg.body,
type: msg.type,
timestamp: msg.timestamp ? new Date(msg.timestamp * 1000).toISOString() : null,
author: msg.author || msg.from,
fromMe: msg.fromMe,
hasMedia: msg.hasMedia,
}));
res.status(200).json({
status: 'success',
groupId: groupId,
messages: formattedMessages,
});
} catch (error) {
logger.error(`[WA] Error getting messages for group:`, error);
res.status(500).json({
status: 'error',
message: 'Failed to get group messages',
error: error instanceof Error ? error.message : String(error),
});
}
});
// 9. CREATE GROUP ENDPOINT - Maps to create_group tool
app.post('/api/groups', async (req: Request, res: Response) => {
try {
if (!ensureClientReady(res)) return;
const { name, participants } = req.body;
if (!name || !participants || !Array.isArray(participants)) {
return res.status(400).json({
status: 'error',
message: 'Missing name or participants array in request body',
});
}
const whatsappClient = state.client;
const result = await whatsappClient.createGroup(name, participants);
res.status(200).json({
status: 'success',
group: {
id: result.gid._serialized,
name: name,
participants: participants,
},
});
} catch (error) {
logger.error('[WA] Error creating group:', error);
res.status(500).json({
status: 'error',
message: 'Failed to create group',
error: error instanceof Error ? error.message : String(error),
});
}
});
// 10. ADD PARTICIPANTS TO GROUP ENDPOINT - Maps to add_participants_to_group tool
app.post('/api/groups/:groupId/participants', async (req: Request, res: Response) => {
try {
if (!ensureClientReady(res)) return;
const { groupId } = req.params;
const { participants } = req.body;
if (!participants || !Array.isArray(participants)) {
return res.status(400).json({
status: 'error',
message: 'Missing participants array in request body',
});
}
const whatsappClient = state.client;
// Get the group chat by ID
const chat = await whatsappClient.getChatById(groupId);
if (!chat || !chat.isGroup) {
return res.status(404).json({
status: 'error',
message: `Group with ID ${groupId} not found`,
});
}
// Add participants
const result = await chat.addParticipants(participants);
res.status(200).json({
status: 'success',
groupId: groupId,
added: result,
});
} catch (error) {
logger.error(`[WA] Error adding participants to group:`, error);
res.status(500).json({
status: 'error',
message: 'Failed to add participants to group',
error: error instanceof Error ? error.message : String(error),
});
}
});
// Add environment variables endpoint for troubleshooting
app.get('/container-env', (_req: Request, res: Response) => {
try {
// Don't log or expose sensitive values
const sanitizedEnv = Object.fromEntries(
Object.entries(process.env)
.filter(
([key]) =>
!key.toLowerCase().includes('key') &&
!key.toLowerCase().includes('token') &&
!key.toLowerCase().includes('secret') &&
!key.toLowerCase().includes('pass') &&
!key.toLowerCase().includes('auth'),
)
.map(([key, value]) => [key, value]),
);
const envData = {
nodeVersion: process.version,
platform: process.platform,
arch: process.arch,
containerVars: {
PORT: process.env.PORT,
NODE_ENV: process.env.NODE_ENV,
DOCKER_CONTAINER: process.env.DOCKER_CONTAINER,
RENDER: process.env.RENDER,
},
// Include sanitized env for debugging only
fullEnv: sanitizedEnv,
timestamp: new Date().toISOString(),
};
logger.info('[WA] Container environment report');
res.status(200).json(envData);
} catch (error) {
logger.error('[WA] Error in container-env endpoint:', error);
res.status(500).send('Error getting container environment');
}
});
// Add file system exploration endpoint for troubleshooting
app.get('/filesys', (_req: Request, res: Response) => {
try {
const directoriesToCheck = [
'/',
'/app',
'/app/data',
'/app/data/whatsapp',
'/var',
'/var/data',
'/var/data/whatsapp',
'/tmp',
'/tmp/puppeteer_data',
];
const fsData = directoriesToCheck.map(dir => {
try {
const exists = fs.existsSync(dir);
let files: string[] = [];
let stats = null;
if (exists) {
try {
stats = fs.statSync(dir);
files = fs.readdirSync(dir).slice(0, 20); // Only get first 20 files
} catch (e) {
files = [`Error reading directory: ${e instanceof Error ? e.message : String(e)}`];
}
}
return {
directory: dir,
exists,
stats: stats
? {
isDirectory: stats.isDirectory(),
size: stats.size,
mode: stats.mode,
uid: stats.uid,
gid: stats.gid,
}
: null,
files,
};
} catch (e) {
return {
directory: dir,
error: e instanceof Error ? e.message : String(e),
};
}
});
logger.info('[WA] File system exploration report');
res.status(200).json(fsData);
} catch (error) {
logger.error('[WA] Error in filesys endpoint:', error);
res.status(500).send('Error exploring file system');
}
});
// Add start WhatsApp endpoint - separated from server start
app.get('/start-whatsapp', (_req: Request, res: Response) => {
// Only start once
if (state.whatsappInitStarted) {
return res.status(200).json({
status: 'WhatsApp initialization already started',
clientReady: state.clientReady,
error: state.whatsappError ? state.whatsappError.message : null,
});
}
// Start WhatsApp initialization
state.whatsappInitStarted = true;
state.whatsappInitializing = true;
// Launch initialization in the background
initializeWhatsAppClient(whatsAppConfig, state);
return res.status(200).json({
status: 'WhatsApp initialization started',
message: 'Check /status for updates',
});
});
// Start server IMMEDIATELY - BEFORE client initialization
// This is CRITICAL to prevent Render deployment failures
const serverPort = port || parseInt(process.env.PORT || '') || 3000;
logger.info(`[WA] Starting HTTP server on port ${serverPort}`);
const server = app.listen(serverPort, '0.0.0.0', () => {
logger.info(`[WA] WhatsApp Web Client API server started on port ${serverPort}`);
});
// Set additional error handlers for process
process.on('uncaughtException', error => {
logger.error('[WA] Uncaught exception:', error);
// Don't crash the server
});
process.on('unhandledRejection', reason => {
logger.error('[WA] Unhandled rejection:', reason);
// Don't crash the server
});
// Keep the process running
process.on('SIGINT', async () => {
logger.info('[WA] Shutting down WhatsApp Web Client API...');
server.close();
process.exit(0);
});
}
// Separate function to initialize WhatsApp client
async function initializeWhatsAppClient(whatsAppConfig: WhatsAppConfig, state: any): Promise<void> {
let client: Client | null = null;
try {
logger.info('[WA] Starting WhatsApp client initialization...');
// Create the client
client = createWhatsAppClient(whatsAppConfig);
// Capture the QR code
client.on('qr', qr => {
logger.info('[WA] New QR code received');
state.latestQrCode = qr;
// QR code file saving is handled in whatsapp-client.ts
// Also log QR code to console for terminal access
try {
// Use a smaller QR code with proper formatting
logger.info('[WA] Scan this QR code with your WhatsApp app:');
const qrcodeTerminal = require('qrcode-terminal');
qrcodeTerminal.generate(qr, { small: true }, function (qrcode: string) {
// Split the QR code by lines and log each line separately to preserve formatting
const qrLines = qrcode.split('\n');
qrLines.forEach((line: string) => {
logger.info(`[WA-QR] ${line}`);
});
});
} catch (error) {
logger.error('[WA] Failed to generate terminal QR code', error);
}
});
client.on('ready', () => {
state.clientReady = true;
state.whatsappInitializing = false;
logger.info('[WA] Client is ready');
});
client.on('auth_failure', error => {
state.whatsappError = new Error(`Authentication failed: ${error}`);
logger.error('[WA] Authentication failed:', error);
});
client.on('disconnected', reason => {
logger.warn('[WA] Client disconnected:', reason);
state.clientReady = false;
});
await client.initialize();
} catch (error) {
state.whatsappInitializing = false;
state.whatsappError = error as Error;
logger.error('[WA] Error during client initialization:', error);
// Don't throw here - we want the server to keep running even if WhatsApp fails
}
}
async function startMcpServer(
mcpConfig: McpConfig,
transport: string,
port: number,
mode: string,
): Promise<void> {
let client: Client | null = null;
if (mode === 'standalone') {
logger.info('Starting WhatsApp Web Client...');
client = createWhatsAppClient(mcpConfig.whatsappConfig);
await client.initialize();
}
logger.info(`Starting MCP server in ${mode} mode...`);
logger.debug('MCP Configuration:', mcpConfig);
const server = createMcpServer(mcpConfig, client);
if (transport === 'sse') {
await startMcpSseServer(server, port, mode);
} else if (transport === 'command') {
await startMcpCommandServer(server, mode);
}
}
async function main(): Promise<void> {
try {
const argv = parseCommandLineArgs();
configureLogger(argv);
const { whatsAppConfig, mcpConfig } = createConfigurations(argv);
if (argv.mode === 'mcp') {
await startMcpServer(
mcpConfig,
argv['transport'] as string,
argv['sse-port'] as number,
argv['mcp-mode'] as string,
);
} else if (argv.mode === 'whatsapp-api') {
await startWhatsAppApiServer(whatsAppConfig, argv['api-port'] as number);
}
} catch (error) {
logger.error('Error starting application:', error);
process.exit(1);
}
}
main();
```