# Directory Structure
```
├── .gitignore
├── Dockerfile
├── mcp-example.json
├── package-lock.json
├── package.json
├── README.md
├── smithery.yaml
├── src
│ ├── database.ts
│ ├── main.ts
│ ├── mcp.ts
│ └── whatsapp.ts
└── tsconfig.json
```
# Files
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
# Node dependencies
node_modules/
# Build output
dist/
# Logs
*.log
wa-logs.txt
mcp-logs.txt
# Baileys authentication data (IMPORTANT!)
auth_info/
# SQLite Database (keep data local)
data/
# Environment variables
.env
# OS generated files
.DS_Store
Thumbs.db
.roo
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
# WhatsApp MCP Server (TypeScript/Baileys)
[](https://smithery.ai/server/@jlucaso1/whatsapp-mcp-ts)
This is a Model Context Protocol (MCP) server for WhatsApp, built with TypeScript and using the `@whiskeysockets/baileys` library.
It allows you to connect your personal WhatsApp account to an AI agent (like Anthropic Claude via its desktop app or Cursor) enabling it to:
* Search your personal WhatsApp messages.
* Search your contacts (individuals, not groups).
* List your recent chats.
* Retrieve message history for specific chats.
* Send messages to individuals or groups.
It connects directly to your **personal WhatsApp account** using the WhatsApp Web multi-device API. All your messages and authentication details are stored **locally** in a SQLite database (`./data/`) and authentication cache (`./auth_info/`). Data is only sent to the connected AI agent when it explicitly uses the provided MCP tools (which you control via the agent's interface).
*(Optional: Consider adding a screenshot or GIF similar to the reference example here)*
## Example
**User:**
Send a whatsapp message to "Meu amor" in whatsapp saying "Te amo"
---
**Assistant:**
Okay, I need to find the contact first.
*Using tool: `whatsapp.search_contacts`*
```json
{
"query": "Meu amor"
}
```
---
**Tool Result:**
```json
[
{
"jid": "[email protected]",
"name": "Meu Amor"
}
]
```
---
**Assistant:**
Found the contact. Now sending the message.
*Using tool: `whatsapp.send_message`*
```json
{
"recipient": "[email protected]",
"message": "Te amo"
}
```
---
**Tool Result:**
```text
Message sent successfully to [email protected] (ID: XXXXXXXXXXX).
```
## Key Features (MCP Tools)
The server exposes the following tools to the connected AI agent:
* `search_contacts`: Search for contacts by name or phone number part (JID).
* `list_messages`: Retrieve message history for a specific chat, with pagination.
* `list_chats`: List your chats, sortable by activity or name, filterable, paginated, optionally includes last message details.
* `get_chat`: Get detailed information about a specific chat.
* `get_message_context`: Retrieve messages sent immediately before and after a specific message ID for context.
* `send_message`: Send a text message to a specified recipient JID (user or group).
## Installation
### Installing via Smithery
To install WhatsApp MCP Server for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@jlucaso1/whatsapp-mcp-ts):
```bash
npx -y @smithery/cli install @jlucaso1/whatsapp-mcp-ts --client claude
```
### Prerequisites
* **Node.js:** Version 23.10.0 or higher (as specified in `package.json`). You can check your version with `node -v`. (Has initial typescript and sqlite builtin support)
* **npm** (or yarn/pnpm): Usually comes with Node.js.
* **AI Client:** Anthropic Claude Desktop app, Cursor, Cline or Roo Code (or another MCP-compatible client).
### Steps
1. **Clone this repository:**
```bash
git clone <your-repo-url> whatsapp-mcp-ts
cd whatsapp-mcp-ts
```
2. **Install dependencies:**
```bash
npm install
# or yarn install / pnpm install
```
3. **Run the server for the first time:**
Use `node` to run the main script directly.
```bash
node src/main.ts
```
* The first time you run it, it will likely generate a QR code link using `quickchart.io` and attempt to open it in your default browser.
* Scan this QR code using your WhatsApp mobile app (Settings > Linked Devices > Link a Device).
* Authentication credentials will be saved locally in the `auth_info/` directory (this is ignored by git).
* Messages will start syncing and be stored in `./data/whatsapp.db`. This might take some time depending on your history size. Check the `wa-logs.txt` and console output for progress.
* Keep this terminal window running. After syncing you can close.
## Configuration for AI Client
You need to tell your AI client how to start this MCP server.
1. **Prepare the configuration JSON:**
Copy the following JSON structure. You'll need to replace `{{PATH_TO_REPO}}` with the **absolute path** to the directory where you cloned this repository.
```json
{
"mcpServers": {
"whatsapp": {
"command": "node",
"args": [
"{{PATH_TO_REPO}}/src/main.ts"
],
"timeout": 15, // Optional: Adjust startup timeout if needed
"disabled": false
}
}
}
```
* **Get the absolute path:** Navigate to the `whatsapp-mcp-ts` directory in your terminal and run `pwd`. Use this output for `{{PATH_TO_REPO}}`.
2. **Save the configuration file:**
* For **Claude Desktop:** Save the JSON as `claude_desktop_config.json` in its configuration directory:
* macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
* Windows: `%APPDATA%\Claude\claude_desktop_config.json` (Likely path, verify if needed)
* Linux: `~/.config/Claude/claude_desktop_config.json` (Likely path, verify if needed)
* For **Cursor:** Save the JSON as `mcp.json` in its configuration directory:
* `~/.cursor/mcp.json`
3. **Restart Claude Desktop / Cursor:**
Close and reopen your AI client. It should now detect the "whatsapp" MCP server and allow you to use its tools.
## Usage
Once the server is running (either manually via `node src/main.ts` or started by the AI client via the config file) and connected to your AI client, you can interact with your WhatsApp data through the agent's chat interface. Ask it to search contacts, list recent chats, read messages, or send messages.
## Architecture Overview
This application is a single Node.js process that:
1. Uses `@whiskeysockets/baileys` to connect to the WhatsApp Web API, handling authentication and real-time events.
2. Stores WhatsApp chats and messages locally in a SQLite database (`./data/whatsapp.db`) using `node:sqlite`.
3. Runs an MCP server using `@modelcontextprotocol/sdk` that listens for requests from an AI client over standard input/output (stdio).
4. Provides MCP tools that query the local SQLite database or use the Baileys socket to send messages.
5. Uses `pino` for logging activity (`wa-logs.txt` for WhatsApp events, `mcp-logs.txt` for MCP server activity).
## Data Storage & Privacy
* **Authentication:** Your WhatsApp connection credentials are stored locally in the `./auth_info/` directory.
* **Messages & Chats:** Your message history and chat metadata are stored locally in the `./data/whatsapp.db` SQLite file.
* **Local Data:** Both `auth_info/` and `data/` are included in `.gitignore` to prevent accidental commits. **Treat these directories as sensitive.**
* **LLM Interaction:** Data is only sent to the connected Large Language Model (LLM) when the AI agent actively uses one of the provided MCP tools (e.g., `list_messages`, `send_message`). The server itself does not proactively send your data anywhere else.
## Technical Details
* **Language:** TypeScript
* **Runtime:** Node.js (>= v23.10.0)
* **WhatsApp API:** `@whiskeysockets/baileys`
* **MCP SDK:** `@modelcontextprotocol/sdk`
* **Database:** `node:sqlite` (Bundled SQLite)
* **Logging:** `pino`
* **Schema Validation:** `zod` (for MCP tool inputs)
## Troubleshooting
* **QR Code Issues:**
* If the QR code link doesn't open automatically, check the console output for the `quickchart.io` URL and open it manually.
* Ensure you scan the QR code promptly with your phone's WhatsApp app.
* **Authentication Failures / Logged Out:**
* If the connection closes with a `DisconnectReason.loggedOut` error, you need to re-authenticate. Stop the server, delete the `./auth_info/` directory, and restart the server (`node src/main.ts`) to get a new QR code.
* **Message Sync Issues:**
* Initial sync can take time. Check `wa-logs.txt` for activity.
* If messages seem out of sync or missing, you might need a full reset. Stop the server, delete **both** `./auth_info/` and `./data/` directories, then restart the server to re-authenticate and resync history.
* **MCP Connection Problems (Claude/Cursor):**
* Double-check the `command` and `args` (especially the `{{PATH_TO_REPO}}`) in your `claude_desktop_config.json` or `mcp.json`. Ensure the path is absolute and correct.
* Verify Node.js are correctly installed and in your system's PATH.
* Check the AI client's logs for errors related to starting the MCP server.
* Check this server's logs (`mcp-logs.txt`) for MCP-related errors.
* **Errors Sending Messages:**
* Ensure the recipient JID is correct (e.g., `[email protected]` for users, `[email protected]` for groups).
* Check `wa-logs.txt` for specific errors from Baileys.
* **General Issues:** Check both `wa-logs.txt` and `mcp-logs.txt` for detailed error messages.
For further MCP integration issues, refer to the [official MCP documentation](https://modelcontextprotocol.io/quickstart/server#claude-for-desktop-integration-issues).
## Credits
- https://github.com/lharries/whatsapp-mcp Do the same as this codebase but uses go and python.
## License
This project is licensed under the ISC License (see `package.json`).
```
--------------------------------------------------------------------------------
/mcp-example.json:
--------------------------------------------------------------------------------
```json
{
"mcpServers": {
"whatsapp": {
"command": "node",
"args": [
"src/main.ts"
],
"timeout": 15,
"alwaysAllow": [],
"disabled": false
}
}
}
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
{
"compilerOptions": {
"target": "ES2020",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"rootDir": "./src",
"outDir": "./dist",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"resolveJsonModule": true,
"sourceMap": true,
"allowImportingTsExtensions": true,
"noEmit": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
```
--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------
```yaml
# Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml
startCommand:
type: stdio
configSchema:
# JSON Schema defining the configuration options for the MCP.
type: object
default: {}
description: No configuration needed
commandFunction:
# A JS function that produces the CLI command based on the given config to start the MCP on stdio.
|-
(config) => ({ command: 'ts-node', args: ['src/main.ts'], env: {} })
exampleConfig: {}
```
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
```dockerfile
# Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile
FROM node:23-alpine
# Create app directory
WORKDIR /app
# Copy package and source
COPY package.json package-lock.json tsconfig.json ./
COPY src ./src
# Install dependencies and tools
RUN apk add --no-cache python3 make g++ git \
&& npm install \
&& npm install -g ts-node typescript \
&& apk del python3 make g++
# Expose no ports (stdio communication)
CMD ["ts-node", "src/main.ts"]
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
{
"name": "whatsapp-mcp-ts",
"version": "1.0.0",
"main": "src/main.ts",
"type": "module",
"keywords": [],
"author": "jlucaso",
"license": "ISC",
"description": "",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.8.0",
"@whiskeysockets/baileys": "^6.7.16",
"open": "^10.1.0",
"pino": "^9.6.0",
"zod": "^3.24.2"
},
"devDependencies": {
"@types/node": "^22.13.14",
"@types/pino": "^7.0.5",
"typescript": "^5.8.2"
},
"engines": {
"node": ">=23.10.0"
}
}
```
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
```typescript
import { pino } from "pino";
import { initializeDatabase } from "./database.ts";
import { startWhatsAppConnection, type WhatsAppSocket } from "./whatsapp.ts";
import { startMcpServer } from "./mcp.ts";
const waLogger = pino(
{
level: process.env.LOG_LEVEL || "info",
timestamp: pino.stdTimeFunctions.isoTime,
},
pino.destination("./wa-logs.txt")
);
const mcpLogger = pino(
{
level: process.env.LOG_LEVEL || "info",
timestamp: pino.stdTimeFunctions.isoTime,
},
pino.destination("./mcp-logs.txt")
);
async function main() {
mcpLogger.info("Starting WhatsApp MCP Server...");
let whatsappSocket: WhatsAppSocket | null = null;
try {
mcpLogger.info("Initializing database...");
initializeDatabase();
mcpLogger.info("Database initialized successfully.");
mcpLogger.info("Attempting to connect to WhatsApp...");
whatsappSocket = await startWhatsAppConnection(waLogger);
mcpLogger.info("WhatsApp connection process initiated.");
} catch (error: any) {
mcpLogger.fatal(
{ err: error },
"Failed during initialization or WhatsApp connection attempt"
);
process.exit(1);
}
try {
mcpLogger.info("Starting MCP server...");
await startMcpServer(whatsappSocket, mcpLogger, waLogger);
mcpLogger.info("MCP Server started and listening.");
} catch (error: any) {
mcpLogger.fatal({ err: error }, "Failed to start MCP server");
process.exit(1);
}
mcpLogger.info("Application setup complete. Running...");
}
async function shutdown(signal: string) {
mcpLogger.info(`Received ${signal}. Shutting down gracefully...`);
waLogger.flush();
mcpLogger.flush();
process.exit(0);
}
process.on("SIGINT", () => shutdown("SIGINT"));
process.on("SIGTERM", () => shutdown("SIGTERM"));
main().catch((error) => {
mcpLogger.fatal({ err: error }, "Unhandled error during application startup");
waLogger.flush();
mcpLogger.flush();
process.exit(1);
});
```
--------------------------------------------------------------------------------
/src/whatsapp.ts:
--------------------------------------------------------------------------------
```typescript
import {
makeWASocket,
useMultiFileAuthState,
fetchLatestBaileysVersion,
makeCacheableSignalKeyStore,
DisconnectReason,
type WAMessage,
type proto,
isJidGroup,
jidNormalizedUser,
} from "@whiskeysockets/baileys";
import P from "pino";
import path from "node:path";
import open from "open";
import {
initializeDatabase,
storeMessage,
storeChat,
type Message as DbMessage,
} from "./database.ts";
const AUTH_DIR = path.join(import.meta.dirname, "..", "auth_info");
export type WhatsAppSocket = ReturnType<typeof makeWASocket>;
function parseMessageForDb(msg: WAMessage): DbMessage | null {
if (!msg.message || !msg.key || !msg.key.remoteJid) {
return null;
}
let content: string | null = null;
const messageType = Object.keys(msg.message)[0];
if (msg.message.conversation) {
content = msg.message.conversation;
} else if (msg.message.extendedTextMessage?.text) {
content = msg.message.extendedTextMessage.text;
} else if (msg.message.imageMessage?.caption) {
content = `[Image] ${msg.message.imageMessage.caption}`;
} else if (msg.message.videoMessage?.caption) {
content = `[Video] ${msg.message.videoMessage.caption}`;
} else if (msg.message.documentMessage?.caption) {
content = `[Document] ${
msg.message.documentMessage.caption ||
msg.message.documentMessage.fileName ||
""
}`;
} else if (msg.message.audioMessage) {
content = `[Audio]`;
} else if (msg.message.stickerMessage) {
content = `[Sticker]`;
} else if (msg.message.locationMessage?.address) {
content = `[Location] ${msg.message.locationMessage.address}`;
} else if (msg.message.contactMessage?.displayName) {
content = `[Contact] ${msg.message.contactMessage.displayName}`;
} else if (msg.message.pollCreationMessage?.name) {
content = `[Poll] ${msg.message.pollCreationMessage.name}`;
}
if (!content) {
return null;
}
const timestampNum =
typeof msg.messageTimestamp === "number"
? msg.messageTimestamp * 1000
: typeof msg.messageTimestamp === "bigint"
? Number(msg.messageTimestamp) * 1000
: Date.now();
const timestamp = new Date(timestampNum);
let senderJid: string | null | undefined = msg.key.participant;
if (!msg.key.fromMe && !senderJid && !isJidGroup(msg.key.remoteJid)) {
senderJid = msg.key.remoteJid;
}
if (msg.key.fromMe && !isJidGroup(msg.key.remoteJid)) {
senderJid = null;
}
return {
id: msg.key.id!,
chat_jid: msg.key.remoteJid,
sender: senderJid ? jidNormalizedUser(senderJid) : null,
content: content,
timestamp: timestamp,
is_from_me: msg.key.fromMe ?? false,
};
}
export async function startWhatsAppConnection(
logger: P.Logger
): Promise<WhatsAppSocket> {
initializeDatabase();
const { state, saveCreds } = await useMultiFileAuthState(AUTH_DIR);
const { version, isLatest } = await fetchLatestBaileysVersion();
logger.info(`Using WA v${version.join(".")}, isLatest: ${isLatest}`);
const sock = makeWASocket({
version,
logger,
auth: {
creds: state.creds,
keys: makeCacheableSignalKeyStore(state.keys, logger),
},
generateHighQualityLinkPreview: true,
shouldIgnoreJid: (jid) => isJidGroup(jid),
});
sock.ev.process(async (events) => {
if (events["connection.update"]) {
const update = events["connection.update"];
const { connection, lastDisconnect, qr } = update;
if (qr) {
logger.info(
{ qrCodeData: qr },
"QR Code Received. Copy the qrCodeData string and use a QR code generator (e.g., online website) to display and scan it with your WhatsApp app."
);
// for now we roughly open the QR code in a browser
await open(`https://quickchart.io/qr?text=${encodeURIComponent(qr)}`);
}
if (connection === "close") {
const statusCode = (lastDisconnect?.error as any)?.output?.statusCode;
logger.warn(
`Connection closed. Reason: ${
DisconnectReason[statusCode as number] || "Unknown"
}`,
lastDisconnect?.error
);
if (statusCode !== DisconnectReason.loggedOut) {
logger.info("Reconnecting...");
startWhatsAppConnection(logger);
} else {
logger.error(
"Connection closed: Logged Out. Please delete auth_info and restart."
);
process.exit(1);
}
} else if (connection === "open") {
logger.info(`Connection opened. WA user: ${sock.user?.name}`);
console.log("Logged as", sock.user?.name);
}
}
if (events["creds.update"]) {
await saveCreds();
logger.info("Credentials saved.");
}
if (events["messaging-history.set"]) {
const { chats, contacts, messages, isLatest, progress, syncType } =
events["messaging-history.set"];
chats.forEach((chat) =>
storeChat({
jid: chat.id,
name: chat.name,
last_message_time: chat.conversationTimestamp
? new Date(Number(chat.conversationTimestamp) * 1000)
: undefined,
})
);
let storedCount = 0;
messages.forEach((msg) => {
const parsed = parseMessageForDb(msg);
if (parsed) {
storeMessage(parsed);
storedCount++;
}
});
logger.info(`Stored ${storedCount} messages from history sync.`);
}
if (events["messages.upsert"]) {
const { messages, type } = events["messages.upsert"];
logger.info(
{ type, count: messages.length },
"Received messages.upsert event"
);
if (type === "notify" || type === "append") {
for (const msg of messages) {
const parsed = parseMessageForDb(msg);
if (parsed) {
logger.info(
{
msgId: parsed.id,
chatId: parsed.chat_jid,
fromMe: parsed.is_from_me,
sender: parsed.sender,
},
`Storing message: ${parsed.content.substring(0, 50)}...`
);
storeMessage(parsed);
} else {
logger.warn(
{ msgId: msg.key?.id, chatId: msg.key?.remoteJid },
"Skipped storing message (parsing failed or unsupported type)"
);
}
}
}
}
if (events["chats.update"]) {
logger.info(
{ count: events["chats.update"].length },
"Received chats.update event"
);
for (const chatUpdate of events["chats.update"]) {
storeChat({
jid: chatUpdate.id!,
name: chatUpdate.name,
last_message_time: chatUpdate.conversationTimestamp
? new Date(Number(chatUpdate.conversationTimestamp) * 1000)
: undefined,
});
}
}
});
return sock;
}
export async function sendWhatsAppMessage(
logger: P.Logger,
sock: WhatsAppSocket | null,
recipientJid: string,
text: string
): Promise<proto.WebMessageInfo | void> {
if (!sock || !sock.user) {
logger.error(
"Cannot send message: WhatsApp socket not connected or initialized."
);
return;
}
if (!recipientJid) {
logger.error("Cannot send message: Recipient JID is missing.");
return;
}
if (!text) {
logger.error("Cannot send message: Message text is empty.");
return;
}
try {
logger.info(
`Sending message to ${recipientJid}: ${text.substring(0, 50)}...`
);
const normalizedJid = jidNormalizedUser(recipientJid);
const result = await sock.sendMessage(normalizedJid, { text: text });
logger.info({ msgId: result?.key.id }, "Message sent successfully");
return result;
} catch (error) {
logger.error({ err: error, recipientJid }, "Failed to send message");
return;
}
}
```
--------------------------------------------------------------------------------
/src/database.ts:
--------------------------------------------------------------------------------
```typescript
import { DatabaseSync } from "node:sqlite";
import path from "node:path";
import fs from "node:fs";
const DATA_DIR = path.join(import.meta.dirname, "..", "data");
const DB_PATH = path.join(DATA_DIR, "whatsapp.db");
export interface Chat {
jid: string;
name?: string | null;
last_message_time?: Date | null;
last_message?: string | null;
last_sender?: string | null;
last_is_from_me?: boolean | null;
}
export type Message = {
id: string;
chat_jid: string;
sender?: string | null;
content: string;
timestamp: Date;
is_from_me: boolean;
chat_name?: string | null;
};
let dbInstance: DatabaseSync | null = null;
function getDb(): DatabaseSync {
if (!dbInstance) {
if (!fs.existsSync(DATA_DIR)) {
fs.mkdirSync(DATA_DIR, { recursive: true });
}
dbInstance = new DatabaseSync(DB_PATH);
}
return dbInstance;
}
export function initializeDatabase(): DatabaseSync {
const db = getDb();
db.exec("PRAGMA journal_mode = WAL");
db.exec(`
CREATE TABLE IF NOT EXISTS chats (
jid TEXT PRIMARY KEY,
name TEXT,
last_message_time TEXT -- Store dates as ISO strings
);
`);
db.exec(`
CREATE TABLE IF NOT EXISTS messages (
id TEXT,
chat_jid TEXT,
sender TEXT, -- JID of the sender (can be group participant or contact)
content TEXT,
timestamp TEXT, -- Store dates as ISO strings
is_from_me INTEGER, -- Store booleans as 0 or 1
PRIMARY KEY (id, chat_jid),
FOREIGN KEY (chat_jid) REFERENCES chats(jid) ON DELETE CASCADE
);
`);
db.exec(
`CREATE INDEX IF NOT EXISTS idx_messages_timestamp ON messages (timestamp);`,
);
db.exec(
`CREATE INDEX IF NOT EXISTS idx_messages_chat_jid ON messages (chat_jid);`,
);
db.exec(
`CREATE INDEX IF NOT EXISTS idx_messages_sender ON messages (sender);`,
);
db.exec(
`CREATE INDEX IF NOT EXISTS idx_chats_last_message_time ON chats (last_message_time);`,
);
return db;
}
export function storeChat(chat: Partial<Chat> & { jid: string }): void {
const db = getDb();
try {
const stmt = db.prepare(`
INSERT INTO chats (jid, name, last_message_time)
VALUES (@jid, @name, @last_message_time)
ON CONFLICT(jid) DO UPDATE SET
name = COALESCE(excluded.name, name),
last_message_time = COALESCE(excluded.last_message_time, last_message_time)
`);
stmt.run({
jid: chat.jid,
name: chat.name ?? null,
last_message_time:
chat.last_message_time instanceof Date
? chat.last_message_time.toISOString()
: chat.last_message_time === null
? null
: String(chat.last_message_time),
});
} catch (error) {
console.error("Error storing chat:", error);
}
}
export function storeMessage(message: Message): void {
const db = getDb();
try {
storeChat({ jid: message.chat_jid, last_message_time: message.timestamp });
const stmt = db.prepare(`
INSERT OR REPLACE INTO messages (id, chat_jid, sender, content, timestamp, is_from_me)
VALUES (@id, @chat_jid, @sender, @content, @timestamp, @is_from_me)
`);
stmt.run({
id: message.id,
chat_jid: message.chat_jid,
sender: message.sender ?? null,
content: message.content,
timestamp: message.timestamp.toISOString(),
is_from_me: message.is_from_me ? 1 : 0,
});
const updateChatTimeStmt = db.prepare(`
UPDATE chats
SET last_message_time = MAX(COALESCE(last_message_time, '1970-01-01T00:00:00.000Z'), @timestamp)
WHERE jid = @jid
`);
updateChatTimeStmt.run({
timestamp: message.timestamp.toISOString(),
jid: message.chat_jid,
});
} catch (error) {
console.error("Error storing message:", error);
}
}
function parseDateSafe(dateString: string | null | undefined): Date | null {
if (!dateString) return null;
try {
const date = new Date(dateString);
return isNaN(date.getTime()) ? null : date;
} catch (e) {
return null;
}
}
function rowToMessage(row: any): Message {
return {
id: row.id,
chat_jid: row.chat_jid,
sender: row.sender,
content: row.content,
timestamp: parseDateSafe(row.timestamp)!,
is_from_me: Boolean(row.is_from_me),
chat_name: row.chat_name,
};
}
function rowToChat(row: any): Chat {
return {
jid: row.jid,
name: row.name,
last_message_time: parseDateSafe(row.last_message_time),
last_message: row.last_message,
last_sender: row.last_sender,
last_is_from_me:
row.last_is_from_me !== null ? Boolean(row.last_is_from_me) : null,
};
}
export function getMessages(
chatJid: string,
limit: number = 20,
page: number = 0,
): Message[] {
const db = getDb();
try {
const offset = page * limit;
const stmt = db.prepare(`
SELECT m.*, c.name as chat_name
FROM messages m
JOIN chats c ON m.chat_jid = c.jid
WHERE m.chat_jid = ? -- Positional parameter 1
ORDER BY m.timestamp DESC
LIMIT ? -- Positional parameter 2
OFFSET ? -- Positional parameter 3
`);
const rows = stmt.all(chatJid, limit, offset) as any[];
return rows.map(rowToMessage);
} catch (error) {
console.error("Error getting messages:", error);
return [];
}
}
export function getChats(
limit: number = 20,
page: number = 0,
sortBy: "last_active" | "name" = "last_active",
query?: string | null,
includeLastMessage: boolean = true,
): Chat[] {
const db = getDb();
try {
const offset = page * limit;
let sql = `
SELECT
c.jid,
c.name,
c.last_message_time
${
includeLastMessage
? `,
(SELECT m.content FROM messages m WHERE m.chat_jid = c.jid ORDER BY m.timestamp DESC LIMIT 1) as last_message,
(SELECT m.sender FROM messages m WHERE m.chat_jid = c.jid ORDER BY m.timestamp DESC LIMIT 1) as last_sender,
(SELECT m.is_from_me FROM messages m WHERE m.chat_jid = c.jid ORDER BY m.timestamp DESC LIMIT 1) as last_is_from_me
`
: ""
}
FROM chats c
`;
const params: (string | number)[] = [];
if (query) {
sql += ` WHERE (LOWER(c.name) LIKE LOWER(?) OR c.jid LIKE ?)`;
params.push(`%${query}%`, `%${query}%`);
}
const orderByClause =
sortBy === "last_active"
? "c.last_message_time DESC NULLS LAST"
: "c.name ASC";
sql += ` ORDER BY ${orderByClause}, c.jid ASC`;
sql += ` LIMIT ? OFFSET ?`;
params.push(limit, offset);
const stmt = db.prepare(sql);
const rows = stmt.all(...params) as any[];
return rows.map(rowToChat);
} catch (error) {
console.error("Error getting chats:", error);
return [];
}
}
export function getChat(
jid: string,
includeLastMessage: boolean = true,
): Chat | null {
const db = getDb();
try {
let sql = `
SELECT
c.jid,
c.name,
c.last_message_time
${
includeLastMessage
? `,
(SELECT m.content FROM messages m WHERE m.chat_jid = c.jid ORDER BY m.timestamp DESC LIMIT 1) as last_message,
(SELECT m.sender FROM messages m WHERE m.chat_jid = c.jid ORDER BY m.timestamp DESC LIMIT 1) as last_sender,
(SELECT m.is_from_me FROM messages m WHERE m.chat_jid = c.jid ORDER BY m.timestamp DESC LIMIT 1) as last_is_from_me
`
: ""
}
FROM chats c
WHERE c.jid = ? -- Positional parameter 1
`;
const stmt = db.prepare(sql);
const row = stmt.get(jid) as any | undefined;
return row ? rowToChat(row) : null;
} catch (error) {
console.error("Error getting chat:", error);
return null;
}
}
export function getMessagesAround(
messageId: string,
before: number = 5,
after: number = 5,
): { before: Message[]; target: Message | null; after: Message[] } {
const db = getDb();
const result: {
before: Message[];
target: Message | null;
after: Message[];
} = { before: [], target: null, after: [] };
try {
const targetStmt = db.prepare(`
SELECT m.*, c.name as chat_name
FROM messages m
JOIN chats c ON m.chat_jid = c.jid
WHERE m.id = ? -- Positional parameter 1
`);
const targetRow = targetStmt.get(messageId) as any | undefined;
if (!targetRow) {
return result;
}
result.target = rowToMessage(targetRow);
const targetTimestamp = result.target.timestamp.toISOString();
const chatJid = result.target.chat_jid;
const beforeStmt = db.prepare(`
SELECT m.*, c.name as chat_name
FROM messages m
JOIN chats c ON m.chat_jid = c.jid
WHERE m.chat_jid = ? AND m.timestamp < ? -- Positional params 1, 2
ORDER BY m.timestamp DESC
LIMIT ? -- Positional param 3
`);
const beforeRows = beforeStmt.all(
chatJid,
targetTimestamp,
before,
) as any[];
result.before = beforeRows.map(rowToMessage).reverse();
const afterStmt = db.prepare(`
SELECT m.*, c.name as chat_name
FROM messages m
JOIN chats c ON m.chat_jid = c.jid
WHERE m.chat_jid = ? AND m.timestamp > ? -- Positional params 1, 2
ORDER BY m.timestamp ASC
LIMIT ? -- Positional param 3
`);
const afterRows = afterStmt.all(chatJid, targetTimestamp, after) as any[];
result.after = afterRows.map(rowToMessage);
return result;
} catch (error) {
console.error("Error getting messages around:", error);
return result;
}
}
export function searchDbForContacts(
query: string,
limit: number = 20,
): Pick<Chat, "jid" | "name">[] {
const db = getDb();
try {
const searchPattern = `%${query}%`;
const stmt = db.prepare(`
SELECT DISTINCT jid, name
FROM chats
WHERE (LOWER(name) LIKE LOWER(?) OR jid LIKE ?) -- Positional params 1, 2
AND jid NOT LIKE '%@g.us' -- Exclude groups
ORDER BY name ASC, jid ASC
LIMIT ? -- Positional param 3
`);
const rows = stmt.all(searchPattern, searchPattern, limit) as Pick<
Chat,
"jid" | "name"
>[];
return rows.map((row) => ({ jid: row.jid, name: row.name ?? null }));
} catch (error) {
console.error("Error searching contacts:", error);
return [];
}
}
export function searchMessages(
searchQuery: string,
chatJid?: string | null,
limit: number = 10,
page: number = 0,
): Message[] {
const db = getDb();
try {
const offset = page * limit;
const searchPattern = `%${searchQuery}%`;
let sql = `
SELECT m.*, c.name as chat_name
FROM messages m
JOIN chats c ON m.chat_jid = c.jid
WHERE LOWER(m.content) LIKE LOWER(?) -- Param 1: searchPattern
`;
const params: (string | number | null)[] = [searchPattern];
if (chatJid) {
sql += ` AND m.chat_jid = ?`;
params.push(chatJid);
}
sql += ` ORDER BY m.timestamp DESC`;
sql += ` LIMIT ?`;
params.push(limit);
sql += ` OFFSET ?`;
params.push(offset);
const stmt = db.prepare(sql);
const rows = stmt.all(...params) as any[];
return rows.map(rowToMessage);
} catch (error) {
console.error("Error searching messages:", error);
return [];
}
}
export function closeDatabase(): void {
if (dbInstance) {
try {
dbInstance.close();
dbInstance = null;
console.log("Database connection closed.");
} catch (error) {
console.error("Error closing database:", error);
}
}
}
```
--------------------------------------------------------------------------------
/src/mcp.ts:
--------------------------------------------------------------------------------
```typescript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { jidNormalizedUser } from "@whiskeysockets/baileys";
import {
type Message as DbMessage,
type Chat as DbChat,
getMessages,
getChats,
getChat,
getMessagesAround,
searchDbForContacts,
searchMessages,
} from "./database.ts";
import { sendWhatsAppMessage, type WhatsAppSocket } from "./whatsapp.ts";
import { type P } from "pino";
function formatDbMessageForJson(msg: DbMessage) {
return {
id: msg.id,
chat_jid: msg.chat_jid,
chat_name: msg.chat_name ?? "Unknown Chat",
sender_jid: msg.sender ?? null,
sender_display: msg.sender
? msg.sender.split("@")[0]
: msg.is_from_me
? "Me"
: "Unknown",
content: msg.content,
timestamp: msg.timestamp.toISOString(),
is_from_me: msg.is_from_me,
};
}
function formatDbChatForJson(chat: DbChat) {
return {
jid: chat.jid,
name: chat.name ?? chat.jid.split("@")[0] ?? "Unknown Chat",
is_group: chat.jid.endsWith("@g.us"),
last_message_time: chat.last_message_time?.toISOString() ?? null,
last_message_preview: chat.last_message ?? null,
last_sender_jid: chat.last_sender ?? null,
last_sender_display: chat.last_sender
? chat.last_sender.split("@")[0]
: chat.last_is_from_me
? "Me"
: null,
last_is_from_me: chat.last_is_from_me ?? null,
};
}
export async function startMcpServer(
sock: WhatsAppSocket | null,
mcpLogger: P.Logger,
waLogger: P.Logger,
): Promise<void> {
mcpLogger.info("Initializing MCP server...");
const server = new McpServer({
name: "whatsapp-baileys-ts",
version: "0.1.0",
capabilities: {
tools: {},
resources: {},
},
});
server.tool(
"search_contacts",
{
query: z
.string()
.min(1)
.describe("Search term for contact name or phone number part of JID"),
},
async ({ query }) => {
mcpLogger.info(
`[MCP Tool] Executing search_contacts with query: "${query}"`,
);
try {
const contacts = searchDbForContacts(query, 20);
const formattedContacts = contacts.map((c) => ({
jid: c.jid,
name: c.name ?? c.jid.split("@")[0],
}));
return {
content: [
{
type: "text",
text: JSON.stringify(formattedContacts, null, 2),
},
],
};
} catch (error: any) {
mcpLogger.error(
`[MCP Tool Error] search_contacts failed: ${error.message}`,
);
return {
isError: true,
content: [
{
type: "text",
text: `Error searching contacts: ${error.message}`,
},
],
};
}
},
);
server.tool(
"list_messages",
{
chat_jid: z
.string()
.describe(
"The JID of the chat (e.g., '[email protected]' or '[email protected]')",
),
limit: z
.number()
.int()
.positive()
.optional()
.default(20)
.describe("Max messages per page (default 20)"),
page: z
.number()
.int()
.nonnegative()
.optional()
.default(0)
.describe("Page number (0-indexed, default 0)"),
},
async ({ chat_jid, limit, page }) => {
mcpLogger.info(
`[MCP Tool] Executing list_messages for chat ${chat_jid}, limit=${limit}, page=${page}`,
);
try {
const messages = getMessages(chat_jid, limit, page);
if (!messages.length && page === 0) {
return {
content: [
{ type: "text", text: `No messages found for chat ${chat_jid}.` },
],
};
} else if (!messages.length) {
return {
content: [
{
type: "text",
text: `No more messages found on page ${page} for chat ${chat_jid}.`,
},
],
};
}
const formattedMessages = messages.map(formatDbMessageForJson);
return {
content: [
{
type: "text",
text: JSON.stringify(formattedMessages, null, 2),
},
],
};
} catch (error: any) {
mcpLogger.error(
`[MCP Tool Error] list_messages failed for ${chat_jid}: ${error.message}`,
);
return {
isError: true,
content: [
{
type: "text",
text: `Error listing messages for ${chat_jid}: ${error.message}`,
},
],
};
}
},
);
server.tool(
"list_chats",
{
limit: z
.number()
.int()
.positive()
.optional()
.default(20)
.describe("Max chats per page (default 20)"),
page: z
.number()
.int()
.nonnegative()
.optional()
.default(0)
.describe("Page number (0-indexed, default 0)"),
sort_by: z
.enum(["last_active", "name"])
.optional()
.default("last_active")
.describe("Sort order: 'last_active' (default) or 'name'"),
query: z
.string()
.optional()
.describe("Optional filter by chat name or JID"),
include_last_message: z
.boolean()
.optional()
.default(true)
.describe("Include last message details (default true)"),
},
async ({ limit, page, sort_by, query, include_last_message }) => {
mcpLogger.info(
`[MCP Tool] Executing list_chats: limit=${limit}, page=${page}, sort=${sort_by}, query=${query}, lastMsg=${include_last_message}`,
);
try {
const chats = getChats(
limit,
page,
sort_by,
query ?? null,
include_last_message,
);
if (!chats.length && page === 0) {
return {
content: [
{
type: "text",
text: `No chats found${query ? ` matching "${query}"` : ""}.`,
},
],
};
} else if (!chats.length) {
return {
content: [
{
type: "text",
text: `No more chats found on page ${page}${
query ? ` matching "${query}"` : ""
}.`,
},
],
};
}
const formattedChats = chats.map(formatDbChatForJson);
return {
content: [
{
type: "text",
text: JSON.stringify(formattedChats, null, 2),
},
],
};
} catch (error: any) {
mcpLogger.error(`[MCP Tool Error] list_chats failed: ${error.message}`);
return {
isError: true,
content: [
{ type: "text", text: `Error listing chats: ${error.message}` },
],
};
}
},
);
server.tool(
"get_chat",
{
chat_jid: z.string().describe("The JID of the chat to retrieve"),
include_last_message: z
.boolean()
.optional()
.default(true)
.describe("Include last message details (default true)"),
},
async ({ chat_jid, include_last_message }) => {
mcpLogger.info(
`[MCP Tool] Executing get_chat for ${chat_jid}, lastMsg=${include_last_message}`,
);
try {
const chat = getChat(chat_jid, include_last_message);
if (!chat) {
return {
isError: true,
content: [
{ type: "text", text: `Chat with JID ${chat_jid} not found.` },
],
};
}
const formattedChat = formatDbChatForJson(chat);
return {
content: [
{
type: "text",
text: JSON.stringify(formattedChat, null, 2),
},
],
};
} catch (error: any) {
mcpLogger.error(
`[MCP Tool Error] get_chat failed for ${chat_jid}: ${error.message}`,
);
return {
isError: true,
content: [
{
type: "text",
text: `Error retrieving chat ${chat_jid}: ${error.message}`,
},
],
};
}
},
);
server.tool(
"get_message_context",
{
message_id: z
.string()
.describe("The ID of the target message to get context around"),
before: z
.number()
.int()
.nonnegative()
.optional()
.default(5)
.describe("Number of messages before (default 5)"),
after: z
.number()
.int()
.nonnegative()
.optional()
.default(5)
.describe("Number of messages after (default 5)"),
},
async ({ message_id, before, after }) => {
mcpLogger.info(
`[MCP Tool] Executing get_message_context for msg ${message_id}, before=${before}, after=${after}`,
);
try {
const context = getMessagesAround(message_id, before, after);
if (!context.target) {
return {
isError: true,
content: [
{
type: "text",
text: `Message with ID ${message_id} not found.`,
},
],
};
}
const formattedContext = {
target: formatDbMessageForJson(context.target),
before: context.before.map(formatDbMessageForJson),
after: context.after.map(formatDbMessageForJson),
};
return {
content: [
{
type: "text",
text: JSON.stringify(formattedContext, null, 2),
},
],
};
} catch (error: any) {
mcpLogger.error(
`[MCP Tool Error] get_message_context failed for ${message_id}: ${error.message}`,
);
return {
isError: true,
content: [
{
type: "text",
text: `Error retrieving context for message ${message_id}: ${error.message}`,
},
],
};
}
},
);
server.tool(
"send_message",
{
recipient: z
.string()
.describe(
"Recipient JID (user or group, e.g., '[email protected]' or '[email protected]')",
),
message: z.string().min(1).describe("The text message to send"),
},
async ({ recipient, message }) => {
mcpLogger.info(`[MCP Tool] Executing send_message to ${recipient}`);
if (!sock) {
mcpLogger.error(
"[MCP Tool Error] send_message failed: WhatsApp socket is not available.",
);
return {
isError: true,
content: [
{ type: "text", text: "Error: WhatsApp connection is not active." },
],
};
}
let normalizedRecipient: string;
try {
normalizedRecipient = jidNormalizedUser(recipient);
if (!normalizedRecipient.includes("@")) {
throw new Error('JID must contain "@" symbol');
}
} catch (normError: any) {
mcpLogger.error(
`[MCP Tool Error] Invalid recipient JID format: ${recipient}. Error: ${normError.message}`,
);
return {
isError: true,
content: [
{
type: "text",
text: `Invalid recipient format: "${recipient}". Please provide a valid JID (e.g., [email protected] or [email protected]).`,
},
],
};
}
try {
const result = await sendWhatsAppMessage(
waLogger,
sock,
normalizedRecipient,
message,
);
if (result && result.key && result.key.id) {
return {
content: [
{
type: "text",
text: `Message sent successfully to ${normalizedRecipient} (ID: ${result.key.id}).`,
},
],
};
} else {
return {
isError: true,
content: [
{
type: "text",
text: `Failed to send message to ${normalizedRecipient}. See server logs for details.`,
},
],
};
}
} catch (error: any) {
mcpLogger.error(
`[MCP Tool Error] send_message failed for ${recipient}: ${error.message}`,
);
return {
isError: true,
content: [
{ type: "text", text: `Error sending message: ${error.message}` },
],
};
}
},
);
server.tool(
"search_messages",
{
query: z
.string()
.min(1)
.describe("The text content to search for within messages"),
chat_jid: z
.string()
.optional()
.describe(
"Optional: The JID of a specific chat to search within (e.g., '123...net' or '[email protected]'). If omitted, searches all chats.",
),
limit: z
.number()
.int()
.positive()
.optional()
.default(10)
.describe("Max messages per page (default 10)"),
page: z
.number()
.int()
.nonnegative()
.optional()
.default(0)
.describe("Page number (0-indexed, default 0)"),
},
async ({ chat_jid, query, limit, page }) => {
const searchScope = chat_jid ? `in chat ${chat_jid}` : "across all chats";
mcpLogger.info(
`[MCP Tool] Executing search_messages ${searchScope}, query="${query}", limit=${limit}, page=${page}`,
);
try {
const messages = searchMessages(query, chat_jid, limit, page);
if (!messages.length && page === 0) {
return {
content: [
{
type: "text",
text: `No messages found containing "${query}" in chat ${chat_jid}.`,
},
],
};
} else if (!messages.length) {
return {
content: [
{
type: "text",
text: `No more messages found containing "${query}" on page ${page} for chat ${chat_jid}.`,
},
],
};
}
const formattedMessages = messages.map(formatDbMessageForJson);
return {
content: [
{
type: "text",
text: JSON.stringify(formattedMessages, null, 2),
},
],
};
} catch (error: any) {
mcpLogger.error(
`[MCP Tool Error] search_messages_in_chat failed for ${chat_jid} / "${query}": ${error.message}`,
);
return {
isError: true,
content: [
{
type: "text",
text: `Error searching messages in chat ${chat_jid}: ${error.message}`,
},
],
};
}
},
);
server.resource("db_schema", "schema://whatsapp/main", async (uri) => {
mcpLogger.info(`[MCP Resource] Request for ${uri.href}`);
const schemaText = `
TABLE chats (jid TEXT PK, name TEXT, last_message_time TIMESTAMP)
TABLE messages (id TEXT, chat_jid TEXT, sender TEXT, content TEXT, timestamp TIMESTAMP, is_from_me BOOLEAN, PK(id, chat_jid), FK(chat_jid) REFERENCES chats(jid))
`.trim();
return {
contents: [
{
uri: uri.href,
text: schemaText,
},
],
};
});
const transport = new StdioServerTransport();
mcpLogger.info("MCP server configured. Connecting stdio transport...");
try {
await server.connect(transport);
mcpLogger.info(
"MCP transport connected. Server is ready and listening via stdio.",
);
} catch (error: any) {
mcpLogger.error(
`[FATAL] Failed to connect MCP transport: ${error.message}`,
error,
);
process.exit(1);
}
mcpLogger.info(
"MCP Server setup complete. Waiting for requests from client...",
);
}
```