@deus-ai/whatsapp-mcp 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,52 @@
1
+ # @deus-ai/whatsapp-mcp
2
+
3
+ Standalone MCP server for WhatsApp messaging. Uses [Baileys](https://github.com/WhiskeySockets/Baileys) for the WhatsApp Web API.
4
+
5
+ Works with any MCP client — Claude Code, Claude Desktop, or your own application.
6
+
7
+ ## Quick Start
8
+
9
+ ```json
10
+ {
11
+ "mcpServers": {
12
+ "whatsapp": {
13
+ "command": "npx",
14
+ "args": ["@deus-ai/whatsapp-mcp"],
15
+ "env": {
16
+ "WHATSAPP_AUTH_DIR": "/path/to/auth/dir"
17
+ }
18
+ }
19
+ }
20
+ }
21
+ ```
22
+
23
+ ## Tools
24
+
25
+ | Tool | Description |
26
+ |------|-------------|
27
+ | `send_message` | Send a text message to a chat |
28
+ | `send_typing` | Show/hide typing indicator |
29
+ | `get_status` | Connection status and identity info |
30
+ | `list_chats` | List known chats and groups |
31
+ | `sync_groups` | Refresh group metadata from WhatsApp |
32
+ | `get_new_messages` | Poll for incoming messages (cursor-based) |
33
+ | `connect` / `disconnect` | Connection lifecycle |
34
+ | `get_auth_status` | Check if WhatsApp credentials exist |
35
+ | `start_auth` | Begin QR or pairing code authentication |
36
+
37
+ ## Incoming Messages
38
+
39
+ Messages are pushed in real-time via MCP logging notifications with `logger: "incoming_message"`. For clients that don't support notifications, use the `get_new_messages` polling tool.
40
+
41
+ ## Environment Variables
42
+
43
+ | Variable | Default | Description |
44
+ |----------|---------|-------------|
45
+ | `WHATSAPP_AUTH_DIR` | `./store/auth` | Path to WhatsApp credential storage |
46
+ | `ASSISTANT_NAME` | `Deus` | Name prefix for outgoing messages |
47
+ | `ASSISTANT_HAS_OWN_NUMBER` | `false` | Skip name prefix when bot has its own phone |
48
+ | `LOG_LEVEL` | `info` | Pino log level (debug, info, warn, error) |
49
+
50
+ ## License
51
+
52
+ MIT
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * WhatsApp MCP Server
4
+ *
5
+ * Standalone MCP server that provides WhatsApp messaging tools.
6
+ * Communicates via stdio (JSON-RPC). Can be used by any MCP client.
7
+ *
8
+ * Config (env vars):
9
+ * WHATSAPP_AUTH_DIR — path to auth credentials (default: ./store/auth)
10
+ * ASSISTANT_NAME — bot display name (default: Deus)
11
+ * ASSISTANT_HAS_OWN_NUMBER — "true" if bot has a dedicated phone number
12
+ * LOG_LEVEL — pino log level (default: info)
13
+ */
14
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,87 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * WhatsApp MCP Server
4
+ *
5
+ * Standalone MCP server that provides WhatsApp messaging tools.
6
+ * Communicates via stdio (JSON-RPC). Can be used by any MCP client.
7
+ *
8
+ * Config (env vars):
9
+ * WHATSAPP_AUTH_DIR — path to auth credentials (default: ./store/auth)
10
+ * ASSISTANT_NAME — bot display name (default: Deus)
11
+ * ASSISTANT_HAS_OWN_NUMBER — "true" if bot has a dedicated phone number
12
+ * LOG_LEVEL — pino log level (default: info)
13
+ */
14
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
15
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
16
+ import { z } from 'zod';
17
+ import { registerCommonTools } from '@deus-ai/channel-core';
18
+ import { WhatsAppProvider } from './whatsapp.js';
19
+ const server = new McpServer({
20
+ name: '@deus-ai/whatsapp-mcp',
21
+ version: '1.0.0',
22
+ });
23
+ const provider = new WhatsAppProvider();
24
+ // Register common tools (send_message, get_status, etc.)
25
+ registerCommonTools(server, provider);
26
+ // ── WhatsApp-specific tools ───────────────────────────────────────────
27
+ server.tool('get_auth_status', 'Check whether WhatsApp credentials exist and the connection is authenticated', {}, async () => {
28
+ const hasAuth = provider.hasAuth();
29
+ const connected = provider.isConnected();
30
+ return {
31
+ content: [
32
+ {
33
+ type: 'text',
34
+ text: JSON.stringify({ has_credentials: hasAuth, connected }),
35
+ },
36
+ ],
37
+ };
38
+ });
39
+ server.tool('start_auth', 'Begin WhatsApp authentication. Returns QR code data or pairing code.', {
40
+ method: z.enum(['qr', 'pairing-code']).describe('Authentication method'),
41
+ phone: z
42
+ .string()
43
+ .optional()
44
+ .describe('Phone number (required for pairing-code method, e.g. 14155551234)'),
45
+ }, async (args) => {
46
+ if (args.method === 'pairing-code' && !args.phone) {
47
+ return {
48
+ content: [
49
+ {
50
+ type: 'text',
51
+ text: JSON.stringify({
52
+ error: 'Phone number required for pairing-code method',
53
+ }),
54
+ },
55
+ ],
56
+ isError: true,
57
+ };
58
+ }
59
+ // Auth is handled by the connect flow — this tool triggers it.
60
+ // The provider writes QR data to disk; the client reads it.
61
+ if (!provider.isConnected()) {
62
+ await provider.connect();
63
+ }
64
+ const status = provider.getStatus();
65
+ return {
66
+ content: [
67
+ {
68
+ type: 'text',
69
+ text: JSON.stringify({
70
+ status: status.connected ? 'connected' : 'authenticating',
71
+ identity: status.identity,
72
+ }),
73
+ },
74
+ ],
75
+ };
76
+ });
77
+ // ── Auto-connect if credentials exist ─────────────────────────────────
78
+ if (provider.hasAuth()) {
79
+ provider.connect().catch((err) => {
80
+ // Log to stderr — don't crash, stay available for auth tools
81
+ console.error('[@deus-ai/whatsapp-mcp] Auto-connect failed:', err.message);
82
+ });
83
+ }
84
+ // ── Start MCP transport ───────────────────────────────────────────────
85
+ const transport = new StdioServerTransport();
86
+ await server.connect(transport);
87
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Standalone WhatsApp connection provider.
3
+ * Extracted from Deus WhatsAppChannel — no Deus-specific dependencies.
4
+ * All config comes from env vars; all messages are forwarded to onMessage.
5
+ */
6
+ import type { ChannelProvider, ChannelStatus, ChatInfo, IncomingMessage } from '@deus-ai/channel-core';
7
+ export declare class WhatsAppProvider implements ChannelProvider {
8
+ readonly name = "whatsapp";
9
+ private sock;
10
+ private connected;
11
+ private connectTime;
12
+ private lidToPhoneMap;
13
+ private outgoingQueue;
14
+ private flushing;
15
+ private groupSyncTimerStarted;
16
+ private sentMessageCache;
17
+ private groupMetadataCache;
18
+ private botLidUser?;
19
+ private pendingFirstOpen?;
20
+ private lastGroupSync;
21
+ private knownChats;
22
+ private readyPromise;
23
+ private readyResolve;
24
+ onMessage: (msg: IncomingMessage) => void;
25
+ connect(): Promise<void>;
26
+ waitForReady(): Promise<void>;
27
+ private connectInternal;
28
+ sendMessage(chatId: string, text: string): Promise<void>;
29
+ isConnected(): boolean;
30
+ getStatus(): ChannelStatus;
31
+ disconnect(): Promise<void>;
32
+ setTyping(chatId: string, isTyping: boolean): Promise<void>;
33
+ listChats(): Promise<ChatInfo[]>;
34
+ syncGroups(): Promise<ChatInfo[]>;
35
+ /** Check if WhatsApp credentials exist on disk. */
36
+ hasAuth(): boolean;
37
+ private syncGroupMetadata;
38
+ private translateJid;
39
+ private getNormalizedGroupMetadata;
40
+ private flushOutgoingQueue;
41
+ }
@@ -0,0 +1,351 @@
1
+ /**
2
+ * Standalone WhatsApp connection provider.
3
+ * Extracted from Deus WhatsAppChannel — no Deus-specific dependencies.
4
+ * All config comes from env vars; all messages are forwarded to onMessage.
5
+ */
6
+ import fs from 'fs';
7
+ import path from 'path';
8
+ import { makeWASocket, Browsers, DisconnectReason, fetchLatestWaWebVersion, makeCacheableSignalKeyStore, normalizeMessageContent, useMultiFileAuthState, } from '@whiskeysockets/baileys';
9
+ import { createRequire } from 'module';
10
+ const { proto } = createRequire(import.meta.url)('@whiskeysockets/baileys');
11
+ import pino from 'pino';
12
+ // ── Config from env vars ────────────────────────────────────────────────
13
+ const AUTH_DIR = process.env.WHATSAPP_AUTH_DIR || path.resolve(process.cwd(), 'store', 'auth');
14
+ const ASSISTANT_NAME = process.env.ASSISTANT_NAME || 'Deus';
15
+ const ASSISTANT_HAS_OWN_NUMBER = process.env.ASSISTANT_HAS_OWN_NUMBER === 'true';
16
+ // Use stderr for logging (stdout is reserved for MCP JSON-RPC)
17
+ const logger = pino({ level: process.env.LOG_LEVEL || 'info' }, pino.destination(2));
18
+ const baileysLogger = pino({ level: 'silent' });
19
+ const GROUP_SYNC_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24 hours
20
+ export class WhatsAppProvider {
21
+ name = 'whatsapp';
22
+ sock;
23
+ connected = false;
24
+ connectTime = 0;
25
+ lidToPhoneMap = {};
26
+ outgoingQueue = [];
27
+ flushing = false;
28
+ groupSyncTimerStarted = false;
29
+ sentMessageCache = new Map();
30
+ groupMetadataCache = new Map();
31
+ botLidUser;
32
+ pendingFirstOpen;
33
+ lastGroupSync = null;
34
+ knownChats = new Map();
35
+ readyPromise = null;
36
+ readyResolve = null;
37
+ // Set by server-base.ts — called for every incoming message
38
+ onMessage = () => { };
39
+ async connect() {
40
+ this.readyPromise = new Promise((resolve) => {
41
+ this.readyResolve = resolve;
42
+ });
43
+ return new Promise((resolve, reject) => {
44
+ this.pendingFirstOpen = resolve;
45
+ this.connectInternal().catch(reject);
46
+ });
47
+ }
48
+ async waitForReady() {
49
+ if (this.connected)
50
+ return;
51
+ if (this.readyPromise)
52
+ await this.readyPromise;
53
+ }
54
+ async connectInternal() {
55
+ fs.mkdirSync(AUTH_DIR, { recursive: true });
56
+ const { state, saveCreds } = await useMultiFileAuthState(AUTH_DIR);
57
+ const { version } = await fetchLatestWaWebVersion({}).catch((err) => {
58
+ logger.warn({ err }, 'Failed to fetch latest WA Web version, using default');
59
+ return { version: undefined };
60
+ });
61
+ this.sock = makeWASocket({
62
+ version,
63
+ auth: {
64
+ creds: state.creds,
65
+ keys: makeCacheableSignalKeyStore(state.keys, baileysLogger),
66
+ },
67
+ printQRInTerminal: false,
68
+ logger: baileysLogger,
69
+ browser: Browsers.macOS('Chrome'),
70
+ cachedGroupMetadata: async (jid) => this.getNormalizedGroupMetadata(jid),
71
+ getMessage: async (key) => {
72
+ const cached = this.sentMessageCache.get(key.id || '');
73
+ if (cached)
74
+ return cached;
75
+ return proto.Message.fromObject({});
76
+ },
77
+ });
78
+ this.sock.ev.on('connection.update', (update) => {
79
+ const { connection, lastDisconnect, qr } = update;
80
+ if (qr) {
81
+ // Write QR data to a file for auth tools to pick up
82
+ const qrPath = path.join(path.dirname(AUTH_DIR), 'qr-data.txt');
83
+ fs.writeFileSync(qrPath, qr);
84
+ logger.warn('WhatsApp authentication required — use start_auth tool');
85
+ }
86
+ if (connection === 'close') {
87
+ this.connected = false;
88
+ const reason = lastDisconnect?.error?.output?.statusCode;
89
+ const shouldReconnect = reason !== DisconnectReason.loggedOut;
90
+ logger.info({ reason, shouldReconnect }, 'Connection closed');
91
+ if (shouldReconnect) {
92
+ logger.info('Reconnecting...');
93
+ this.connectInternal().catch((err) => {
94
+ logger.error({ err }, 'Failed to reconnect, retrying in 5s');
95
+ setTimeout(() => {
96
+ this.connectInternal().catch((err2) => {
97
+ logger.error({ err: err2 }, 'Reconnection retry failed');
98
+ });
99
+ }, 5000);
100
+ });
101
+ }
102
+ else {
103
+ logger.info('Logged out. Re-authenticate to continue.');
104
+ }
105
+ }
106
+ else if (connection === 'open') {
107
+ this.connected = true;
108
+ this.connectTime = Date.now();
109
+ logger.info('Connected to WhatsApp');
110
+ this.sock.sendPresenceUpdate('available').catch(() => { });
111
+ // Build LID to phone mapping
112
+ if (this.sock.user) {
113
+ const phoneUser = this.sock.user.id.split(':')[0];
114
+ const lidUser = this.sock.user.lid?.split(':')[0];
115
+ if (lidUser && phoneUser) {
116
+ this.lidToPhoneMap[lidUser] = `${phoneUser}@s.whatsapp.net`;
117
+ this.botLidUser = lidUser;
118
+ }
119
+ }
120
+ this.flushOutgoingQueue().catch(() => { });
121
+ this.syncGroupMetadata().catch(() => { });
122
+ if (!this.groupSyncTimerStarted) {
123
+ this.groupSyncTimerStarted = true;
124
+ setInterval(() => {
125
+ this.syncGroupMetadata().catch(() => { });
126
+ }, GROUP_SYNC_INTERVAL_MS);
127
+ }
128
+ if (this.pendingFirstOpen) {
129
+ this.pendingFirstOpen();
130
+ this.pendingFirstOpen = undefined;
131
+ }
132
+ if (this.readyResolve) {
133
+ this.readyResolve();
134
+ this.readyResolve = null;
135
+ }
136
+ }
137
+ });
138
+ this.sock.ev.on('creds.update', saveCreds);
139
+ // Phone number share event — not typed in all baileys versions
140
+ this.sock.ev.on('chats.phoneNumberShare', (data) => {
141
+ const lidUser = data.lid?.split('@')[0].split(':')[0];
142
+ if (lidUser && data.jid) {
143
+ this.lidToPhoneMap[lidUser] = data.jid;
144
+ this.groupMetadataCache.clear();
145
+ }
146
+ });
147
+ this.sock.ev.on('messages.upsert', async ({ messages }) => {
148
+ for (const msg of messages) {
149
+ try {
150
+ if (!msg.message)
151
+ continue;
152
+ const normalized = normalizeMessageContent(msg.message);
153
+ if (!normalized)
154
+ continue;
155
+ const rawJid = msg.key.remoteJid;
156
+ if (!rawJid || rawJid === 'status@broadcast')
157
+ continue;
158
+ let chatJid = await this.translateJid(rawJid);
159
+ if (chatJid.endsWith('@lid') && msg.key.senderPn) {
160
+ const pn = msg.key.senderPn;
161
+ const phoneJid = pn.includes('@') ? pn : `${pn}@s.whatsapp.net`;
162
+ this.lidToPhoneMap[rawJid.split('@')[0].split(':')[0]] = phoneJid;
163
+ chatJid = phoneJid;
164
+ }
165
+ const timestamp = new Date(Number(msg.messageTimestamp) * 1000).toISOString();
166
+ const isGroup = chatJid.endsWith('@g.us');
167
+ // Track chat metadata
168
+ this.knownChats.set(chatJid, {
169
+ name: msg.pushName || chatJid.split('@')[0],
170
+ isGroup,
171
+ });
172
+ let content = normalized.conversation ||
173
+ normalized.extendedTextMessage?.text ||
174
+ normalized.imageMessage?.caption ||
175
+ normalized.videoMessage?.caption ||
176
+ '';
177
+ // Normalize bot LID mentions to assistant name
178
+ if (this.botLidUser && content.includes(`@${this.botLidUser}`)) {
179
+ content = content.replace(`@${this.botLidUser}`, `@${ASSISTANT_NAME}`);
180
+ }
181
+ if (!content)
182
+ continue;
183
+ const sender = msg.key.participant || msg.key.remoteJid || '';
184
+ const senderName = msg.pushName || sender.split('@')[0];
185
+ const fromMe = msg.key.fromMe || false;
186
+ const isBotMessage = ASSISTANT_HAS_OWN_NUMBER
187
+ ? fromMe
188
+ : content.startsWith(`${ASSISTANT_NAME}:`);
189
+ // Forward ALL messages — the host decides which to act on
190
+ this.onMessage({
191
+ id: msg.key.id || '',
192
+ chat_id: chatJid,
193
+ sender,
194
+ sender_name: senderName,
195
+ content,
196
+ timestamp,
197
+ is_from_me: fromMe,
198
+ is_group: isGroup,
199
+ metadata: { is_bot_message: isBotMessage },
200
+ });
201
+ }
202
+ catch (err) {
203
+ logger.error({ err, remoteJid: msg.key?.remoteJid }, 'Error processing message');
204
+ }
205
+ }
206
+ });
207
+ }
208
+ async sendMessage(chatId, text) {
209
+ const prefixed = ASSISTANT_HAS_OWN_NUMBER
210
+ ? text
211
+ : `${ASSISTANT_NAME}: ${text}`;
212
+ if (!this.connected) {
213
+ this.outgoingQueue.push({ jid: chatId, text: prefixed });
214
+ return;
215
+ }
216
+ try {
217
+ const sent = await this.sock.sendMessage(chatId, { text: prefixed });
218
+ if (sent?.key?.id && sent.message) {
219
+ this.sentMessageCache.set(sent.key.id, sent.message);
220
+ if (this.sentMessageCache.size > 256) {
221
+ const oldest = this.sentMessageCache.keys().next().value;
222
+ this.sentMessageCache.delete(oldest);
223
+ }
224
+ }
225
+ }
226
+ catch (err) {
227
+ this.outgoingQueue.push({ jid: chatId, text: prefixed });
228
+ logger.warn({ chatId, err }, 'Failed to send, message queued');
229
+ }
230
+ }
231
+ isConnected() {
232
+ return this.connected;
233
+ }
234
+ getStatus() {
235
+ return {
236
+ connected: this.connected,
237
+ channel: 'whatsapp',
238
+ identity: this.sock?.user?.id?.split(':')[0],
239
+ uptime_seconds: this.connected
240
+ ? Math.floor((Date.now() - this.connectTime) / 1000)
241
+ : 0,
242
+ };
243
+ }
244
+ async disconnect() {
245
+ this.connected = false;
246
+ this.sock?.end(undefined);
247
+ }
248
+ async setTyping(chatId, isTyping) {
249
+ try {
250
+ await this.sock.sendPresenceUpdate(isTyping ? 'composing' : 'paused', chatId);
251
+ }
252
+ catch {
253
+ // Best effort
254
+ }
255
+ }
256
+ async listChats() {
257
+ return Array.from(this.knownChats.entries()).map(([id, info]) => ({
258
+ id,
259
+ name: info.name,
260
+ is_group: info.isGroup,
261
+ }));
262
+ }
263
+ async syncGroups() {
264
+ return this.syncGroupMetadata(true);
265
+ }
266
+ /** Check if WhatsApp credentials exist on disk. */
267
+ hasAuth() {
268
+ return fs.existsSync(path.join(AUTH_DIR, 'creds.json'));
269
+ }
270
+ // ── Internal helpers ──────────────────────────────────────────────────
271
+ async syncGroupMetadata(force = false) {
272
+ if (!force && this.lastGroupSync) {
273
+ const elapsed = Date.now() - new Date(this.lastGroupSync).getTime();
274
+ if (elapsed < GROUP_SYNC_INTERVAL_MS)
275
+ return [];
276
+ }
277
+ try {
278
+ const groups = await this.sock.groupFetchAllParticipating();
279
+ const result = [];
280
+ for (const [jid, metadata] of Object.entries(groups)) {
281
+ if (metadata.subject) {
282
+ this.knownChats.set(jid, { name: metadata.subject, isGroup: true });
283
+ result.push({ id: jid, name: metadata.subject, is_group: true });
284
+ }
285
+ }
286
+ this.lastGroupSync = new Date().toISOString();
287
+ logger.info({ count: result.length }, 'Group metadata synced');
288
+ return result;
289
+ }
290
+ catch (err) {
291
+ logger.error({ err }, 'Failed to sync group metadata');
292
+ return [];
293
+ }
294
+ }
295
+ async translateJid(jid) {
296
+ if (!jid.endsWith('@lid'))
297
+ return jid;
298
+ const lidUser = jid.split('@')[0].split(':')[0];
299
+ const cached = this.lidToPhoneMap[lidUser];
300
+ if (cached)
301
+ return cached;
302
+ try {
303
+ const pn = await this.sock.signalRepository?.lidMapping?.getPNForLID(jid);
304
+ if (pn) {
305
+ const phoneJid = `${pn.split('@')[0].split(':')[0]}@s.whatsapp.net`;
306
+ this.lidToPhoneMap[lidUser] = phoneJid;
307
+ return phoneJid;
308
+ }
309
+ }
310
+ catch {
311
+ // Best effort
312
+ }
313
+ return jid;
314
+ }
315
+ async getNormalizedGroupMetadata(jid) {
316
+ if (!jid.endsWith('@g.us'))
317
+ return undefined;
318
+ const cached = this.groupMetadataCache.get(jid);
319
+ if (cached && cached.expiresAt > Date.now())
320
+ return cached.metadata;
321
+ const metadata = await this.sock.groupMetadata(jid);
322
+ const participants = await Promise.all(metadata.participants.map(async (p) => ({
323
+ ...p,
324
+ id: await this.translateJid(p.id),
325
+ })));
326
+ const normalized = { ...metadata, participants };
327
+ this.groupMetadataCache.set(jid, {
328
+ metadata: normalized,
329
+ expiresAt: Date.now() + 60_000,
330
+ });
331
+ return normalized;
332
+ }
333
+ async flushOutgoingQueue() {
334
+ if (this.flushing || this.outgoingQueue.length === 0)
335
+ return;
336
+ this.flushing = true;
337
+ try {
338
+ while (this.outgoingQueue.length > 0) {
339
+ const item = this.outgoingQueue.shift();
340
+ const sent = await this.sock.sendMessage(item.jid, { text: item.text });
341
+ if (sent?.key?.id && sent.message) {
342
+ this.sentMessageCache.set(sent.key.id, sent.message);
343
+ }
344
+ }
345
+ }
346
+ finally {
347
+ this.flushing = false;
348
+ }
349
+ }
350
+ }
351
+ //# sourceMappingURL=whatsapp.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"whatsapp.js","sourceRoot":"","sources":["../src/whatsapp.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@deus-ai/whatsapp-mcp",
3
+ "version": "1.0.0",
4
+ "description": "WhatsApp MCP server — standalone WhatsApp Web integration for any MCP client",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "deus-ai-whatsapp-mcp": "./dist/index.js"
9
+ },
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "start": "node dist/index.js",
13
+ "test": "vitest run"
14
+ },
15
+ "dependencies": {
16
+ "@deus-ai/channel-core": "^1.0.0",
17
+ "@modelcontextprotocol/sdk": "^1.12.1",
18
+ "@whiskeysockets/baileys": "^7.0.0-rc.9",
19
+ "pino": "^10.3.1",
20
+ "qrcode-terminal": "^0.12.0",
21
+ "zod": "^4.3.6"
22
+ },
23
+ "devDependencies": {
24
+ "@types/node": "^22.10.7",
25
+ "@types/qrcode-terminal": "^0.12.0",
26
+ "typescript": "^6.0.2",
27
+ "vitest": "^4.1.2"
28
+ },
29
+ "engines": {
30
+ "node": ">=20"
31
+ },
32
+ "publishConfig": {
33
+ "access": "public"
34
+ },
35
+ "license": "MIT"
36
+ }
package/src/index.ts ADDED
@@ -0,0 +1,111 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * WhatsApp MCP Server
5
+ *
6
+ * Standalone MCP server that provides WhatsApp messaging tools.
7
+ * Communicates via stdio (JSON-RPC). Can be used by any MCP client.
8
+ *
9
+ * Config (env vars):
10
+ * WHATSAPP_AUTH_DIR — path to auth credentials (default: ./store/auth)
11
+ * ASSISTANT_NAME — bot display name (default: Deus)
12
+ * ASSISTANT_HAS_OWN_NUMBER — "true" if bot has a dedicated phone number
13
+ * LOG_LEVEL — pino log level (default: info)
14
+ */
15
+
16
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
17
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
18
+ import { z } from 'zod';
19
+ import { registerCommonTools } from '@deus-ai/channel-core';
20
+
21
+ import { WhatsAppProvider } from './whatsapp.js';
22
+
23
+ const server = new McpServer({
24
+ name: '@deus-ai/whatsapp-mcp',
25
+ version: '1.0.0',
26
+ });
27
+
28
+ const provider = new WhatsAppProvider();
29
+
30
+ // Register common tools (send_message, get_status, etc.)
31
+ registerCommonTools(server, provider);
32
+
33
+ // ── WhatsApp-specific tools ───────────────────────────────────────────
34
+
35
+ server.tool(
36
+ 'get_auth_status',
37
+ 'Check whether WhatsApp credentials exist and the connection is authenticated',
38
+ {},
39
+ async () => {
40
+ const hasAuth = provider.hasAuth();
41
+ const connected = provider.isConnected();
42
+ return {
43
+ content: [
44
+ {
45
+ type: 'text' as const,
46
+ text: JSON.stringify({ has_credentials: hasAuth, connected }),
47
+ },
48
+ ],
49
+ };
50
+ },
51
+ );
52
+
53
+ server.tool(
54
+ 'start_auth',
55
+ 'Begin WhatsApp authentication. Returns QR code data or pairing code.',
56
+ {
57
+ method: z.enum(['qr', 'pairing-code']).describe('Authentication method'),
58
+ phone: z
59
+ .string()
60
+ .optional()
61
+ .describe(
62
+ 'Phone number (required for pairing-code method, e.g. 14155551234)',
63
+ ),
64
+ },
65
+ async (args) => {
66
+ if (args.method === 'pairing-code' && !args.phone) {
67
+ return {
68
+ content: [
69
+ {
70
+ type: 'text' as const,
71
+ text: JSON.stringify({
72
+ error: 'Phone number required for pairing-code method',
73
+ }),
74
+ },
75
+ ],
76
+ isError: true,
77
+ };
78
+ }
79
+ // Auth is handled by the connect flow — this tool triggers it.
80
+ // The provider writes QR data to disk; the client reads it.
81
+ if (!provider.isConnected()) {
82
+ await provider.connect();
83
+ }
84
+ const status = provider.getStatus();
85
+ return {
86
+ content: [
87
+ {
88
+ type: 'text' as const,
89
+ text: JSON.stringify({
90
+ status: status.connected ? 'connected' : 'authenticating',
91
+ identity: status.identity,
92
+ }),
93
+ },
94
+ ],
95
+ };
96
+ },
97
+ );
98
+
99
+ // ── Auto-connect if credentials exist ─────────────────────────────────
100
+
101
+ if (provider.hasAuth()) {
102
+ provider.connect().catch((err) => {
103
+ // Log to stderr — don't crash, stay available for auth tools
104
+ console.error('[@deus-ai/whatsapp-mcp] Auto-connect failed:', err.message);
105
+ });
106
+ }
107
+
108
+ // ── Start MCP transport ───────────────────────────────────────────────
109
+
110
+ const transport = new StdioServerTransport();
111
+ await server.connect(transport);
@@ -0,0 +1,439 @@
1
+ /**
2
+ * Standalone WhatsApp connection provider.
3
+ * Extracted from Deus WhatsAppChannel — no Deus-specific dependencies.
4
+ * All config comes from env vars; all messages are forwarded to onMessage.
5
+ */
6
+
7
+ import fs from 'fs';
8
+ import path from 'path';
9
+
10
+ import {
11
+ makeWASocket,
12
+ Browsers,
13
+ DisconnectReason,
14
+ fetchLatestWaWebVersion,
15
+ makeCacheableSignalKeyStore,
16
+ normalizeMessageContent,
17
+ useMultiFileAuthState,
18
+ } from '@whiskeysockets/baileys';
19
+ import type {
20
+ GroupMetadata,
21
+ WAMessageKey,
22
+ WASocket,
23
+ proto as ProtoTypes,
24
+ } from '@whiskeysockets/baileys';
25
+ import { createRequire } from 'module';
26
+ const { proto } = createRequire(import.meta.url)('@whiskeysockets/baileys') as {
27
+ proto: typeof ProtoTypes;
28
+ };
29
+ import pino from 'pino';
30
+
31
+ import type {
32
+ ChannelProvider,
33
+ ChannelStatus,
34
+ ChatInfo,
35
+ IncomingMessage,
36
+ } from '@deus-ai/channel-core';
37
+
38
+ // ── Config from env vars ────────────────────────────────────────────────
39
+
40
+ const AUTH_DIR =
41
+ process.env.WHATSAPP_AUTH_DIR || path.resolve(process.cwd(), 'store', 'auth');
42
+ const ASSISTANT_NAME = process.env.ASSISTANT_NAME || 'Deus';
43
+ const ASSISTANT_HAS_OWN_NUMBER =
44
+ process.env.ASSISTANT_HAS_OWN_NUMBER === 'true';
45
+
46
+ // Use stderr for logging (stdout is reserved for MCP JSON-RPC)
47
+ const logger = pino(
48
+ { level: process.env.LOG_LEVEL || 'info' },
49
+ pino.destination(2),
50
+ );
51
+ const baileysLogger = pino({ level: 'silent' });
52
+
53
+ const GROUP_SYNC_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24 hours
54
+
55
+ export class WhatsAppProvider implements ChannelProvider {
56
+ readonly name = 'whatsapp';
57
+
58
+ private sock!: WASocket;
59
+ private connected = false;
60
+ private connectTime = 0;
61
+ private lidToPhoneMap: Record<string, string> = {};
62
+ private outgoingQueue: Array<{ jid: string; text: string }> = [];
63
+ private flushing = false;
64
+ private groupSyncTimerStarted = false;
65
+ private sentMessageCache = new Map<string, ProtoTypes.IMessage>();
66
+ private groupMetadataCache = new Map<
67
+ string,
68
+ { metadata: GroupMetadata; expiresAt: number }
69
+ >();
70
+ private botLidUser?: string;
71
+ private pendingFirstOpen?: () => void;
72
+ private lastGroupSync: string | null = null;
73
+ private knownChats = new Map<string, { name: string; isGroup: boolean }>();
74
+ private readyPromise: Promise<void> | null = null;
75
+ private readyResolve: (() => void) | null = null;
76
+
77
+ // Set by server-base.ts — called for every incoming message
78
+ onMessage: (msg: IncomingMessage) => void = () => {};
79
+
80
+ async connect(): Promise<void> {
81
+ this.readyPromise = new Promise<void>((resolve) => {
82
+ this.readyResolve = resolve;
83
+ });
84
+ return new Promise<void>((resolve, reject) => {
85
+ this.pendingFirstOpen = resolve;
86
+ this.connectInternal().catch(reject);
87
+ });
88
+ }
89
+
90
+ async waitForReady(): Promise<void> {
91
+ if (this.connected) return;
92
+ if (this.readyPromise) await this.readyPromise;
93
+ }
94
+
95
+ private async connectInternal(): Promise<void> {
96
+ fs.mkdirSync(AUTH_DIR, { recursive: true });
97
+
98
+ const { state, saveCreds } = await useMultiFileAuthState(AUTH_DIR);
99
+
100
+ const { version } = await fetchLatestWaWebVersion({}).catch((err) => {
101
+ logger.warn(
102
+ { err },
103
+ 'Failed to fetch latest WA Web version, using default',
104
+ );
105
+ return { version: undefined };
106
+ });
107
+
108
+ this.sock = makeWASocket({
109
+ version,
110
+ auth: {
111
+ creds: state.creds,
112
+ keys: makeCacheableSignalKeyStore(state.keys, baileysLogger),
113
+ },
114
+ printQRInTerminal: false,
115
+ logger: baileysLogger,
116
+ browser: Browsers.macOS('Chrome'),
117
+ cachedGroupMetadata: async (jid: string) =>
118
+ this.getNormalizedGroupMetadata(jid),
119
+ getMessage: async (key: WAMessageKey) => {
120
+ const cached = this.sentMessageCache.get(key.id || '');
121
+ if (cached) return cached;
122
+ return proto.Message.fromObject({});
123
+ },
124
+ });
125
+
126
+ this.sock.ev.on('connection.update', (update) => {
127
+ const { connection, lastDisconnect, qr } = update;
128
+
129
+ if (qr) {
130
+ // Write QR data to a file for auth tools to pick up
131
+ const qrPath = path.join(path.dirname(AUTH_DIR), 'qr-data.txt');
132
+ fs.writeFileSync(qrPath, qr);
133
+ logger.warn('WhatsApp authentication required — use start_auth tool');
134
+ }
135
+
136
+ if (connection === 'close') {
137
+ this.connected = false;
138
+ const reason = (
139
+ lastDisconnect?.error as { output?: { statusCode?: number } }
140
+ )?.output?.statusCode;
141
+ const shouldReconnect = reason !== DisconnectReason.loggedOut;
142
+ logger.info({ reason, shouldReconnect }, 'Connection closed');
143
+
144
+ if (shouldReconnect) {
145
+ logger.info('Reconnecting...');
146
+ this.connectInternal().catch((err) => {
147
+ logger.error({ err }, 'Failed to reconnect, retrying in 5s');
148
+ setTimeout(() => {
149
+ this.connectInternal().catch((err2) => {
150
+ logger.error({ err: err2 }, 'Reconnection retry failed');
151
+ });
152
+ }, 5000);
153
+ });
154
+ } else {
155
+ logger.info('Logged out. Re-authenticate to continue.');
156
+ }
157
+ } else if (connection === 'open') {
158
+ this.connected = true;
159
+ this.connectTime = Date.now();
160
+ logger.info('Connected to WhatsApp');
161
+
162
+ this.sock.sendPresenceUpdate('available').catch(() => {});
163
+
164
+ // Build LID to phone mapping
165
+ if (this.sock.user) {
166
+ const phoneUser = this.sock.user.id.split(':')[0];
167
+ const lidUser = this.sock.user.lid?.split(':')[0];
168
+ if (lidUser && phoneUser) {
169
+ this.lidToPhoneMap[lidUser] = `${phoneUser}@s.whatsapp.net`;
170
+ this.botLidUser = lidUser;
171
+ }
172
+ }
173
+
174
+ this.flushOutgoingQueue().catch(() => {});
175
+ this.syncGroupMetadata().catch(() => {});
176
+
177
+ if (!this.groupSyncTimerStarted) {
178
+ this.groupSyncTimerStarted = true;
179
+ setInterval(() => {
180
+ this.syncGroupMetadata().catch(() => {});
181
+ }, GROUP_SYNC_INTERVAL_MS);
182
+ }
183
+
184
+ if (this.pendingFirstOpen) {
185
+ this.pendingFirstOpen();
186
+ this.pendingFirstOpen = undefined;
187
+ }
188
+ if (this.readyResolve) {
189
+ this.readyResolve();
190
+ this.readyResolve = null;
191
+ }
192
+ }
193
+ });
194
+
195
+ this.sock.ev.on('creds.update', saveCreds);
196
+
197
+ // Phone number share event — not typed in all baileys versions
198
+ (this.sock.ev as any).on(
199
+ 'chats.phoneNumberShare',
200
+ (data: { lid?: string; jid?: string }) => {
201
+ const lidUser = data.lid?.split('@')[0].split(':')[0];
202
+ if (lidUser && data.jid) {
203
+ this.lidToPhoneMap[lidUser] = data.jid;
204
+ this.groupMetadataCache.clear();
205
+ }
206
+ },
207
+ );
208
+
209
+ this.sock.ev.on('messages.upsert', async ({ messages }) => {
210
+ for (const msg of messages) {
211
+ try {
212
+ if (!msg.message) continue;
213
+ const normalized = normalizeMessageContent(msg.message);
214
+ if (!normalized) continue;
215
+ const rawJid = msg.key.remoteJid;
216
+ if (!rawJid || rawJid === 'status@broadcast') continue;
217
+
218
+ let chatJid = await this.translateJid(rawJid);
219
+ if (chatJid.endsWith('@lid') && (msg.key as any).senderPn) {
220
+ const pn = (msg.key as any).senderPn as string;
221
+ const phoneJid = pn.includes('@') ? pn : `${pn}@s.whatsapp.net`;
222
+ this.lidToPhoneMap[rawJid.split('@')[0].split(':')[0]] = phoneJid;
223
+ chatJid = phoneJid;
224
+ }
225
+
226
+ const timestamp = new Date(
227
+ Number(msg.messageTimestamp) * 1000,
228
+ ).toISOString();
229
+ const isGroup = chatJid.endsWith('@g.us');
230
+
231
+ // Track chat metadata
232
+ this.knownChats.set(chatJid, {
233
+ name: msg.pushName || chatJid.split('@')[0],
234
+ isGroup,
235
+ });
236
+
237
+ let content =
238
+ normalized.conversation ||
239
+ normalized.extendedTextMessage?.text ||
240
+ normalized.imageMessage?.caption ||
241
+ normalized.videoMessage?.caption ||
242
+ '';
243
+
244
+ // Normalize bot LID mentions to assistant name
245
+ if (this.botLidUser && content.includes(`@${this.botLidUser}`)) {
246
+ content = content.replace(
247
+ `@${this.botLidUser}`,
248
+ `@${ASSISTANT_NAME}`,
249
+ );
250
+ }
251
+
252
+ if (!content) continue;
253
+
254
+ const sender = msg.key.participant || msg.key.remoteJid || '';
255
+ const senderName = msg.pushName || sender.split('@')[0];
256
+ const fromMe = msg.key.fromMe || false;
257
+ const isBotMessage = ASSISTANT_HAS_OWN_NUMBER
258
+ ? fromMe
259
+ : content.startsWith(`${ASSISTANT_NAME}:`);
260
+
261
+ // Forward ALL messages — the host decides which to act on
262
+ this.onMessage({
263
+ id: msg.key.id || '',
264
+ chat_id: chatJid,
265
+ sender,
266
+ sender_name: senderName,
267
+ content,
268
+ timestamp,
269
+ is_from_me: fromMe,
270
+ is_group: isGroup,
271
+ metadata: { is_bot_message: isBotMessage },
272
+ });
273
+ } catch (err) {
274
+ logger.error(
275
+ { err, remoteJid: msg.key?.remoteJid },
276
+ 'Error processing message',
277
+ );
278
+ }
279
+ }
280
+ });
281
+ }
282
+
283
+ async sendMessage(chatId: string, text: string): Promise<void> {
284
+ const prefixed = ASSISTANT_HAS_OWN_NUMBER
285
+ ? text
286
+ : `${ASSISTANT_NAME}: ${text}`;
287
+
288
+ if (!this.connected) {
289
+ this.outgoingQueue.push({ jid: chatId, text: prefixed });
290
+ return;
291
+ }
292
+ try {
293
+ const sent = await this.sock.sendMessage(chatId, { text: prefixed });
294
+ if (sent?.key?.id && sent.message) {
295
+ this.sentMessageCache.set(sent.key.id, sent.message);
296
+ if (this.sentMessageCache.size > 256) {
297
+ const oldest = this.sentMessageCache.keys().next().value!;
298
+ this.sentMessageCache.delete(oldest);
299
+ }
300
+ }
301
+ } catch (err) {
302
+ this.outgoingQueue.push({ jid: chatId, text: prefixed });
303
+ logger.warn({ chatId, err }, 'Failed to send, message queued');
304
+ }
305
+ }
306
+
307
+ isConnected(): boolean {
308
+ return this.connected;
309
+ }
310
+
311
+ getStatus(): ChannelStatus {
312
+ return {
313
+ connected: this.connected,
314
+ channel: 'whatsapp',
315
+ identity: this.sock?.user?.id?.split(':')[0],
316
+ uptime_seconds: this.connected
317
+ ? Math.floor((Date.now() - this.connectTime) / 1000)
318
+ : 0,
319
+ };
320
+ }
321
+
322
+ async disconnect(): Promise<void> {
323
+ this.connected = false;
324
+ this.sock?.end(undefined);
325
+ }
326
+
327
+ async setTyping(chatId: string, isTyping: boolean): Promise<void> {
328
+ try {
329
+ await this.sock.sendPresenceUpdate(
330
+ isTyping ? 'composing' : 'paused',
331
+ chatId,
332
+ );
333
+ } catch {
334
+ // Best effort
335
+ }
336
+ }
337
+
338
+ async listChats(): Promise<ChatInfo[]> {
339
+ return Array.from(this.knownChats.entries()).map(([id, info]) => ({
340
+ id,
341
+ name: info.name,
342
+ is_group: info.isGroup,
343
+ }));
344
+ }
345
+
346
+ async syncGroups(): Promise<ChatInfo[]> {
347
+ return this.syncGroupMetadata(true);
348
+ }
349
+
350
+ /** Check if WhatsApp credentials exist on disk. */
351
+ hasAuth(): boolean {
352
+ return fs.existsSync(path.join(AUTH_DIR, 'creds.json'));
353
+ }
354
+
355
+ // ── Internal helpers ──────────────────────────────────────────────────
356
+
357
+ private async syncGroupMetadata(force = false): Promise<ChatInfo[]> {
358
+ if (!force && this.lastGroupSync) {
359
+ const elapsed = Date.now() - new Date(this.lastGroupSync).getTime();
360
+ if (elapsed < GROUP_SYNC_INTERVAL_MS) return [];
361
+ }
362
+
363
+ try {
364
+ const groups = await this.sock.groupFetchAllParticipating();
365
+ const result: ChatInfo[] = [];
366
+ for (const [jid, metadata] of Object.entries(groups)) {
367
+ if (metadata.subject) {
368
+ this.knownChats.set(jid, { name: metadata.subject, isGroup: true });
369
+ result.push({ id: jid, name: metadata.subject, is_group: true });
370
+ }
371
+ }
372
+ this.lastGroupSync = new Date().toISOString();
373
+ logger.info({ count: result.length }, 'Group metadata synced');
374
+ return result;
375
+ } catch (err) {
376
+ logger.error({ err }, 'Failed to sync group metadata');
377
+ return [];
378
+ }
379
+ }
380
+
381
+ private async translateJid(jid: string): Promise<string> {
382
+ if (!jid.endsWith('@lid')) return jid;
383
+ const lidUser = jid.split('@')[0].split(':')[0];
384
+ const cached = this.lidToPhoneMap[lidUser];
385
+ if (cached) return cached;
386
+
387
+ try {
388
+ const pn = await (
389
+ this.sock.signalRepository as any
390
+ )?.lidMapping?.getPNForLID(jid);
391
+ if (pn) {
392
+ const phoneJid = `${pn.split('@')[0].split(':')[0]}@s.whatsapp.net`;
393
+ this.lidToPhoneMap[lidUser] = phoneJid;
394
+ return phoneJid;
395
+ }
396
+ } catch {
397
+ // Best effort
398
+ }
399
+ return jid;
400
+ }
401
+
402
+ private async getNormalizedGroupMetadata(
403
+ jid: string,
404
+ ): Promise<GroupMetadata | undefined> {
405
+ if (!jid.endsWith('@g.us')) return undefined;
406
+ const cached = this.groupMetadataCache.get(jid);
407
+ if (cached && cached.expiresAt > Date.now()) return cached.metadata;
408
+
409
+ const metadata = await this.sock.groupMetadata(jid);
410
+ const participants = await Promise.all(
411
+ metadata.participants.map(async (p) => ({
412
+ ...p,
413
+ id: await this.translateJid(p.id),
414
+ })),
415
+ );
416
+ const normalized = { ...metadata, participants };
417
+ this.groupMetadataCache.set(jid, {
418
+ metadata: normalized,
419
+ expiresAt: Date.now() + 60_000,
420
+ });
421
+ return normalized;
422
+ }
423
+
424
+ private async flushOutgoingQueue(): Promise<void> {
425
+ if (this.flushing || this.outgoingQueue.length === 0) return;
426
+ this.flushing = true;
427
+ try {
428
+ while (this.outgoingQueue.length > 0) {
429
+ const item = this.outgoingQueue.shift()!;
430
+ const sent = await this.sock.sendMessage(item.jid, { text: item.text });
431
+ if (sent?.key?.id && sent.message) {
432
+ this.sentMessageCache.set(sent.key.id, sent.message);
433
+ }
434
+ }
435
+ } finally {
436
+ this.flushing = false;
437
+ }
438
+ }
439
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "outDir": "./dist",
7
+ "rootDir": "./src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "declaration": true,
12
+ "sourceMap": true
13
+ },
14
+ "include": ["src/**/*"],
15
+ "exclude": ["node_modules", "dist"]
16
+ }