feat: whatsapp-mcp-go

This commit is contained in:
iamatulsingh
2026-02-21 21:12:14 +01:00
commit dd5e347436
35 changed files with 4159 additions and 0 deletions
+22
View File
@@ -0,0 +1,22 @@
MIT License
Copyright (c) 2025 Luke Harries
Copyright (c) 2026 Atul Singh
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+263
View File
@@ -0,0 +1,263 @@
# WhatsAppMCP (Go Edition)
A lightweight, WhatsApp MCP server and bridge — rewritten entirely in Go for simplicity, portability, and realworld automation workflows.
This project is a reimagining of the original **[whatsapp-mcp](https://github.com/lharries/whatsapp-mcp)** by **lharries**, who created the first WhatsApp MCP bridge using a Python MCP server and a Go WhatsApp client powered by **WhatsMeow**. Their work demonstrated how Claude Desktop could interact with WhatsApp through the MCP protocol, and this project would not exist without that foundation.
Start `whatsapp-bridge` -> then run `whatsapp-mcp-server` in your preferred mode (STDIO or SSE).
| Connector | Chat |
|------------|----------|
| ![Connector](./screenshots/connector_claude.png) | ![Chat](./screenshots/chat_example.png) |
## Installation
### Prerequisites
- Go
- Anthropic Claude Desktop app (or Cursor) or `n8n` workflow
- FFmpeg (_optional_) - Only needed for audio messages. If you want to send audio files as playable WhatsApp voice messages, they must be in `.ogg` Opus format. With FFmpeg installed, the MCP server will automatically convert non-Opus audio files. Without FFmpeg, you can still send raw audio files using the `send_file` tool.
### Steps
1. **Clone this repository**
```bash
git clone https://github.com/iamatulsingh/whatsapp-mcp.git
cd whatsapp-mcp
```
2. **Run the WhatsApp bridge**
Navigate to the whatsapp-bridge directory and run the Go application:
```bash
cd whatsapp-bridge
go run main.go
```
The first time you run it, you will be prompted to scan a QR code. Scan the QR code with your WhatsApp mobile app to authenticate.
After approximately 20 days, you will might need to re-authenticate.
3. **Connect to the MCP server**
```bash
cd whatsapp-mcp-server
go build -o whatsapp-mcp
```
Copy the below json with the appropriate {{PATH}} values:
```json
{
"mcpServers": {
"whatsapp-mcp": {
"command": "{{PROJECT_BASE_PATH}}/whatsapp-mcp-server/whatsapp-mcp"
}
},
"preferences": {
"sidebarMode": "chat",
"coworkScheduledTasksEnabled": false
}
}
```
For **Claude**, save this as `claude_desktop_config.json` in your Claude Desktop configuration directory at:
```
~/Library/Application Support/Claude/claude_desktop_config.json
```
For **Cursor**, save this as `mcp.json` in your Cursor configuration directory at:
```
~/.cursor/mcp.json
```
### Windows Compatibility
If you're running this project on Windows, be aware that `go-sqlite3` requires **CGO to be enabled** in order to compile and work properly. By default, **CGO is disabled on Windows**, so you need to explicitly enable it and have a C compiler installed.
#### Steps to get it working:
1. **Install a C compiler**
We recommend using [MSYS2](https://www.msys2.org/) to install a C compiler for Windows. After installing MSYS2, make sure to add the `ucrt64\bin` folder to your `PATH`.
→ A step-by-step guide is available [here](https://code.visualstudio.com/docs/cpp/config-mingw).
2. **Enable CGO and run the app**
```bash
cd whatsapp-bridge
go env -w CGO_ENABLED=1
go run main.go
```
OR
Simply use dokcer compose to do all the job for you
```bash
docker compose up
```
## Architecture Overview
This application consists of two main components:
1. **WhatsApp Bridge** (`whatsapp-bridge/`): A Go application that connects to WhatsApp's web API, handles authentication via QR code, and stores message history in SQLite. It serves as the bridge between WhatsApp and the MCP server.
2. **MCP Server** (`whatsapp-mcp-server/`): A Go implemention of the Model Context Protocol (MCP), which provides standardized tools for Claude to interact with WhatsApp data and send/receive messages.
### Data Storage
- All message history is stored in `postgres` by default and you'll need to create a database name `whatsapp` OR a SQLite database within the `whatsapp-bridge/store/` directory
- The database maintains tables for chats and messages
- Messages are indexed for efficient searching and retrieval
### MCP Tools
Claude can access the following tools to interact with WhatsApp:
- **search_contacts**: Search for contacts by name or phone number
- **list_messages**: Retrieve messages with optional filters and context
- **list_chats**: List available chats with metadata
- **get_chat**: Get information about a specific chat
- **get_direct_chat_by_contact**: Find a direct chat with a specific contact
- **get_contact_chats**: List all chats involving a specific contact
- **get_last_interaction**: Get the most recent message with a contact
- **get_message_context**: Retrieve context around a specific message
- **send_message**: Send a WhatsApp message to a specified phone number or group JID
- **send_file**: Send a file (image, video, raw audio, document) to a specified recipient
- **send_audio_message**: Send an audio file as a WhatsApp voice message (requires the file to be an .ogg opus file or ffmpeg must be installed)
- **download_media**: Download media from a WhatsApp message and get the local file path
### Media Handling Features
The MCP server supports both sending and receiving various media types:
#### Media Sending
You can send various media types to your WhatsApp contacts:
- **Images, Videos, Documents**: Use the `send_file` tool to share any supported media type.
- **Voice Messages**: Use the `send_audio_message` tool to send audio files as playable WhatsApp voice messages.
- For optimal compatibility, audio files should be in `.ogg` Opus format.
- With FFmpeg installed, the system will automatically convert other audio formats (MP3, WAV, etc.) to the required format.
- Without FFmpeg, you can still send raw audio files using the `send_file` tool, but they won't appear as playable voice messages.
#### Media Downloading
By default, just the metadata of the media is stored in the local database. The message will indicate that media was sent. To access this media you need to use the download_media tool which takes the `message_id` and `chat_jid` (which are shown when printing messages containing the meda), this downloads the media and then returns the file path which can be then opened or passed to another tool.
## Technical Details
1. Claude sends requests to the MCP server
2. The MCP server queries the Go bridge for WhatsApp data or directly to the SQLite database
3. The Bridge accesses the WhatsApp API and keeps the SQLite or Postgres database up to date
4. Data flows back through the chain to Claude
5. When sending messages, the request flows from Claude through the MCP server to the bridge and to WhatsApp
## Troubleshooting
- Make sure both the Bridge application and the MCP server are running for the integration to work properly.
- In case of the Client outdated (405) error in the Bridge Go server,
```bash
06:13:28.434 [Client INFO] Starting WhatsApp client...
2025/07/29 06:13:28 Connecting to postgres
06:13:30.402 [Client ERROR] Client outdated (405) connect failure (client version: 2.3000.1021018791)
06:13:30.403 [Client/Socket ERROR] Error reading from websocket: websocket: close 1006 (abnormal closure): unexpecte
06:13:30.675 [Client ERROR] Failed to establish stable connection
```
Run below command and fix any error in your code after upgrade and the bridge code will automatic update the whatsapp client version.
```bash
go get -u go.mau.fi/whatsmeow@latest
```
OR Update all packages
```bash
go get -u
```
### Authentication Issues
- **QR Code Not Displaying**: If the QR code doesn't appear, try restarting the authentication script. If issues persist, check if your terminal supports displaying QR codes.
- **WhatsApp Already Logged In**: If your session is already active, the Go bridge will automatically reconnect without showing a QR code.
- **Device Limit Reached**: WhatsApp limits the number of linked devices. If you reach this limit, you'll need to remove an existing device from WhatsApp on your phone (Settings > Linked Devices).
- **No Messages Loading**: After initial authentication, it can take several minutes for your message history to load, especially if you have many chats.
- **WhatsApp Out of Sync**: If your WhatsApp messages get out of sync with the bridge, delete both database files (`whatsapp-bridge/store/messages.db` and `whatsapp-bridge/store/whatsapp.db`) and restart the bridge to re-authenticate.
This fork takes the core idea and rebuilds it with a different philosophy:
**one language, one binary, clean architecture, and flexible deployment.**
---
## Why This Project Exists
The original project is excellent, but it was designed specifically for **Claude Desktop**, and its architecture had a few constraints that made extending or deploying it more difficult. This fork was created to solve those pain points.
* ***Single Language: Everything in Go**
The original design split responsibilities: This dramatically simplifies development and distribution.
* **Clean Architecture: No Mixed Database Logic**
Moving all database logic into the bridge
* **SQLite or PostgreSQL — Your Choice**
The original project only supported SQLite.
* **Two Communication Modes: STDIO + SSE** The original project was built only for Claude Desktop (STDIO MCP). This makes the project usable far beyond Claude Desktop.
* **Docker Support** This makes deployment trivial on:
- servers
- containers
- orchestrators
- local development
---
## Features
- Full WhatsApp client using **WhatsMeow**
- Pure Go MCP server
- Clean API boundary between MCP and bridge
- SQLite or PostgreSQL support
- STDIO + SSE modes
- Lightweight Docker image
- Easy deployment with Docker Compose
- No Python dependencies
- No mixed database logic
- Productionready architecture
---
## Credits
This project stands on the shoulders of two major contributors:
### **Original Author: lharries**
The initial idea, architecture, and implementation of WhatsAppMCP came from
**https://github.com/lharries/whatsapp-mcp**
Their work demonstrated how MCP could be used to bridge WhatsApp and Claude Desktop.
This fork would not exist without their project.
### **WhatsMeow**
The WhatsApp client functionality is powered by the incredible Go library:
**https://github.com/tulir/whatsmeow**
Without WhatsMeow, none of this would be possible.
---
## Why You Might Use This Fork
Choose this version if you want:
- a cleaner architecture
- a singlelanguage codebase
- a portable binary
- Docker support
- PostgreSQL support
- SSE support for automation workflows
- a more maintainable and extensible project
The goal is not to replace the original project but to offer an alternative that fits different needs — especially for developers who prefer Go or want to deploy MCPbased WhatsApp automation in production environments.
Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

+7
View File
@@ -0,0 +1,7 @@
.idea
.vscode
manifests
Dockerfile
README.md
Makefile
store
+1
View File
@@ -0,0 +1 @@
media
+10
View File
@@ -0,0 +1,10 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Environment-dependent path to Maven home directory
/mavenHomeManager.xml
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
+25
View File
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="whatsapp.db" uuid="4f6200ec-1eae-4549-9d93-2a328888b8d7">
<driver-ref>sqlite.xerial</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
<jdbc-url>jdbc:sqlite:$PROJECT_DIR$/store/whatsapp.db</jdbc-url>
<jdbc-additional-properties>
<property name="com.intellij.clouds.kubernetes.db.enabled" value="false" />
</jdbc-additional-properties>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
<data-source source="LOCAL" name="messages.db" uuid="ae157347-e4f8-43bd-a8b5-8556d3489f79">
<driver-ref>sqlite.xerial</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
<jdbc-url>jdbc:sqlite:$PROJECT_DIR$/store/messages.db</jdbc-url>
<jdbc-additional-properties>
<property name="com.intellij.clouds.kubernetes.db.enabled" value="false" />
</jdbc-additional-properties>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
</component>
</project>
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourcePerFileMappings">
<file url="file://$APPLICATION_CONFIG_DIR$/consoles/db/4f6200ec-1eae-4549-9d93-2a328888b8d7/console.sql" value="4f6200ec-1eae-4549-9d93-2a328888b8d7" />
</component>
</project>
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="db-tree-configuration">
<option name="data" value="----------------------------------------&#10;1:0:4f6200ec-1eae-4549-9d93-2a328888b8d7&#10;2:0:ae157347-e4f8-43bd-a8b5-8556d3489f79&#10;" />
</component>
</project>
+11
View File
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GoImports">
<option name="excludedPackages">
<array>
<option value="github.com/pkg/errors" />
<option value="golang.org/x/net/context" />
</array>
</option>
</component>
</project>
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" languageLevel="JDK_23" default="true" project-jdk-name="23" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/whatsapp-bridge.iml" filepath="$PROJECT_DIR$/.idea/whatsapp-bridge.iml" />
</modules>
</component>
</project>
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="SqlDialectMappings">
<file url="file://$PROJECT_DIR$/main.go" dialect="GenericSQL" />
</component>
</project>
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/../../../.." vcs="Git" />
</component>
</project>
+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="Go" enabled="true" />
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
+30
View File
@@ -0,0 +1,30 @@
# Build the manager binary
FROM golang:1.25.7 AS builder
ARG TARGETARCH
ARG TARGETOS
WORKDIR /project
# Copy the Go Modules manifests
COPY go.mod go.mod
COPY go.sum go.sum
# cache deps before building and copying source so that we don't need to re-download as much
# and so that source changes don't invalidate our downloaded layer
RUN go mod download
# Copy the go source
COPY . .
# Build
RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} GO111MODULE=on go build -a -o whatsapp_backup main.go
# Use distroless as minimal base image to package the manager binary
# Refer to https://github.com/GoogleContainerTools/distroless for more details
FROM gcr.io/distroless/static-debian12:nonroot
WORKDIR /project
COPY --from=builder /project/whatsapp_backup .
USER 65532:65532
ENTRYPOINT ["/project/whatsapp_backup"]
+24
View File
@@ -0,0 +1,24 @@
VERSION ?= 0.0.1
all: install build
install:
go get ./...
build:
go build
run:
go run main.go
tidy:
go mod tidy
test:
go test ./...
image:
docker buildx build --platform linux/amd64,linux/arm64 -t venomdarklord/wa:v$(VERSION) --push .
deployment:
kubectl apply -k ./manifests
+30
View File
@@ -0,0 +1,30 @@
version: "3.9"
services:
wa:
build:
context: .
dockerfile: Dockerfile
container_name: wa
restart: always
environment:
IS_POSTGRES: "true"
POSTGRES_USER: "test"
POSTGRES_PASS: "test"
POSTGRES_HOST: "192.168.1.100"
POSTGRES_PORT: "5432"
ports:
- "8080:8080"
volumes:
- media:/project/store
deploy:
resources:
limits:
cpus: "1"
memory: "128M"
reservations:
cpus: "0.5"
memory: "64M"
volumes:
media:
+33
View File
@@ -0,0 +1,33 @@
module whatsapp-bridge
go 1.25.0
require (
github.com/lib/pq v1.11.2
github.com/mattn/go-sqlite3 v1.14.34
github.com/mdp/qrterminal v1.0.1
go.mau.fi/whatsmeow v0.0.0-20260219150138-7ae702b1eed4
google.golang.org/protobuf v1.36.11
)
require (
filippo.io/edwards25519 v1.2.0 // indirect
github.com/beeper/argo-go v1.1.2 // indirect
github.com/coder/websocket v1.8.14 // indirect
github.com/elliotchance/orderedmap/v3 v3.1.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/petermattis/goid v0.0.0-20260113132338-7c7de50cc741 // indirect
github.com/rs/zerolog v1.34.0 // indirect
github.com/vektah/gqlparser/v2 v2.5.32 // indirect
go.mau.fi/libsignal v0.2.1 // indirect
go.mau.fi/util v0.9.6 // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa // indirect
golang.org/x/net v0.50.0 // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/text v0.34.0 // indirect
rsc.io/qr v0.2.0 // indirect
)
+116
View File
@@ -0,0 +1,116 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
github.com/beeper/argo-go v1.1.2 h1:UQI2G8F+NLfGTOmTUI0254pGKx/HUU/etbUGTJv91Fs=
github.com/beeper/argo-go v1.1.2/go.mod h1:M+LJAnyowKVQ6Rdj6XYGEn+qcVFkb3R/MUpqkGR0hM4=
github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=
github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/elliotchance/orderedmap/v3 v3.1.0 h1:j4DJ5ObEmMBt/lcwIecKcoRxIQUEnw0L804lXYDt/pg=
github.com/elliotchance/orderedmap/v3 v3.1.0/go.mod h1:G+Hc2RwaZvJMcS4JpGCOyViCnGeKf0bTYCGTO4uhjSo=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lib/pq v1.11.2 h1:x6gxUeu39V0BHZiugWe8LXZYZ+Utk7hSJGThs8sdzfs=
github.com/lib/pq v1.11.2/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.29 h1:1O6nRLJKvsi1H2Sj0Hzdfojwt8GiGKm+LOfLaBFaouQ=
github.com/mattn/go-sqlite3 v1.14.29/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mattn/go-sqlite3 v1.14.34 h1:3NtcvcUnFBPsuRcno8pUtupspG/GM+9nZ88zgJcp6Zk=
github.com/mattn/go-sqlite3 v1.14.34/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mdp/qrterminal v1.0.1 h1:07+fzVDlPuBlXS8tB0ktTAyf+Lp1j2+2zK3fBOL5b7c=
github.com/mdp/qrterminal v1.0.1/go.mod h1:Z33WhxQe9B6CdW37HaVqcRKzP+kByF3q/qLxOGe12xQ=
github.com/petermattis/goid v0.0.0-20250508124226-395b08cebbdb h1:3PrKuO92dUTMrQ9dx0YNejC6U/Si6jqKmyQ9vWjwqR4=
github.com/petermattis/goid v0.0.0-20250508124226-395b08cebbdb/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
github.com/petermattis/goid v0.0.0-20251121121749-a11dd1a45f9a h1:VweslR2akb/ARhXfqSfRbj1vpWwYXf3eeAUyw/ndms0=
github.com/petermattis/goid v0.0.0-20251121121749-a11dd1a45f9a/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
github.com/petermattis/goid v0.0.0-20260113132338-7c7de50cc741 h1:KPpdlQLZcHfTMQRi6bFQ7ogNO0ltFT4PmtwTLW4W+14=
github.com/petermattis/goid v0.0.0-20260113132338-7c7de50cc741/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/vektah/gqlparser/v2 v2.5.31 h1:YhWGA1mfTjID7qJhd1+Vxhpk5HTgydrGU9IgkWBTJ7k=
github.com/vektah/gqlparser/v2 v2.5.31/go.mod h1:c1I28gSOVNzlfc4WuDlqU7voQnsqI6OG2amkBAFmgts=
github.com/vektah/gqlparser/v2 v2.5.32 h1:k9QPJd4sEDTL+qB4ncPLflqTJ3MmjB9SrVzJrawpFSc=
github.com/vektah/gqlparser/v2 v2.5.32/go.mod h1:c1I28gSOVNzlfc4WuDlqU7voQnsqI6OG2amkBAFmgts=
go.mau.fi/libsignal v0.2.0 h1:oRXj3OHhEJq51BFEM8/50UZblmWiTYH93hsNTPcbk90=
go.mau.fi/libsignal v0.2.0/go.mod h1:tvjoDsMejgT38CXTXwqaYu8itBiY8O2Mb6biWvZBb9k=
go.mau.fi/libsignal v0.2.1 h1:vRZG4EzTn70XY6Oh/pVKrQGuMHBkAWlGRC22/85m9L0=
go.mau.fi/libsignal v0.2.1/go.mod h1:iVvjrHyfQqWajOUaMEsIfo3IqgVMrhWcPiiEzk7NgoU=
go.mau.fi/util v0.8.8 h1:OnuEEc/sIJFhnq4kFggiImUpcmnmL/xpvQMRu5Fiy5c=
go.mau.fi/util v0.8.8/go.mod h1:Y/kS3loxTEhy8Vill513EtPXr+CRDdae+Xj2BXXMy/c=
go.mau.fi/util v0.9.4 h1:gWdUff+K2rCynRPysXalqqQyr2ahkSWaestH6YhSpso=
go.mau.fi/util v0.9.4/go.mod h1:647nVfwUvuhlZFOnro3aRNPmRd2y3iDha9USb8aKSmM=
go.mau.fi/util v0.9.6 h1:2nsvxm49KhI3wrFltr0+wSUBlnQ4CMtykuELjpIU+ts=
go.mau.fi/util v0.9.6/go.mod h1:sIJpRH7Iy5Ad1SBuxQoatxtIeErgzxCtjd/2hCMkYMI=
go.mau.fi/whatsmeow v0.0.0-20250723174453-937d77661333 h1:wZnqbIembr69yyUeWWEjxNxeofhYg3PDEx1BRRvHdWM=
go.mau.fi/whatsmeow v0.0.0-20250723174453-937d77661333/go.mod h1:ltDTXUgOAT7LcFKp11H+5S7UY7+xHBMGzNJcv3dLHGk=
go.mau.fi/whatsmeow v0.0.0-20251217143725-11cf47c62d32 h1:NeE9eEYY4kEJVCfCXaAU27LgAPugPHRHJdC9IpXFPzI=
go.mau.fi/whatsmeow v0.0.0-20251217143725-11cf47c62d32/go.mod h1:S4OWR9+hTx+54+jRzl+NfRBXnGpPm5IRPyhXB7haSd0=
go.mau.fi/whatsmeow v0.0.0-20260219150138-7ae702b1eed4 h1:hsmlwsM+VqfF70cpdZEeIUKer2XWCQmQPK0u0tHy3ZQ=
go.mau.fi/whatsmeow v0.0.0-20260219150138-7ae702b1eed4/go.mod h1:mXCRFyPEPn4jqWz6Afirn8vY7DpHCPnlKq6I2cWwFHM=
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc h1:TS73t7x3KarrNd5qAipmspBDS1rkMcgVG/fS1aRb4Rc=
golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc/go.mod h1:A+z0yzpGtvnG90cToK5n2tu8UJVP2XUATh+r+sfOOOc=
golang.org/x/exp v0.0.0-20251209150349-8475f28825e9 h1:MDfG8Cvcqlt9XXrmEiD4epKn7VJHZO84hejP9Jmp0MM=
golang.org/x/exp v0.0.0-20251209150349-8475f28825e9/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU=
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0=
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY=
rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs=
File diff suppressed because it is too large Load Diff
+10
View File
@@ -0,0 +1,10 @@
# Default ignored files
/shelf/
/workspace.xml
# Ignored default folder with query files
/queries/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
# Editor-based HTTP Client requests
/httpRequests/
+11
View File
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GoImports">
<option name="excludedPackages">
<array>
<option value="github.com/pkg/errors" />
<option value="golang.org/x/net/context" />
</array>
</option>
</component>
</project>
+7
View File
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="KubernetesApiProvider">{}</component>
<component name="ProjectRootManager" version="2">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/whatsapp-mcp-server-golang.iml" filepath="$PROJECT_DIR$/.idea/whatsapp-mcp-server-golang.iml" />
</modules>
</component>
</project>
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/../../../.." vcs="Git" />
</component>
</project>
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="Go" enabled="true" />
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
+17
View File
@@ -0,0 +1,17 @@
module whatsapp-mcp-server
go 1.25.0
require (
github.com/mattn/go-sqlite3 v1.14.34
github.com/modelcontextprotocol/go-sdk v1.3.1
)
require (
github.com/google/jsonschema-go v0.4.2 // indirect
github.com/segmentio/asm v1.2.1 // indirect
github.com/segmentio/encoding v0.5.3 // indirect
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
golang.org/x/oauth2 v0.35.0 // indirect
golang.org/x/sys v0.41.0 // indirect
)
+22
View File
@@ -0,0 +1,22 @@
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8=
github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
github.com/mattn/go-sqlite3 v1.14.34 h1:3NtcvcUnFBPsuRcno8pUtupspG/GM+9nZ88zgJcp6Zk=
github.com/mattn/go-sqlite3 v1.14.34/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/modelcontextprotocol/go-sdk v1.3.1 h1:TfqtNKOIWN4Z1oqmPAiWDC2Jq7K9OdJaooe0teoXASI=
github.com/modelcontextprotocol/go-sdk v1.3.1/go.mod h1:DgVX498dMD8UJlseK1S5i1T4tFz2fkBk4xogC3D15nw=
github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0=
github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
github.com/segmentio/encoding v0.5.3 h1:OjMgICtcSFuNvQCdwqMCv9Tg7lEOXGwm1J5RPQccx6w=
github.com/segmentio/encoding v0.5.3/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0=
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=
golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
+92
View File
@@ -0,0 +1,92 @@
package helpers
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
)
// ConvertToOpusOggTemp creates a temporary .ogg file in Opus format
// Uses reasonable defaults for WhatsApp voice messages
func ConvertToOpusOggTemp(inputFile string) (string, error) {
// WhatsApp voice messages usually use:
// bitrate → 24k32k is very common and good quality/size balance
// sample rate → 48000 Hz (Opus native)
const defaultBitrate = "32k"
const defaultSampleRate = 48000
return ConvertToOpusOggTempWithParams(inputFile, defaultBitrate, defaultSampleRate)
}
// ConvertToOpusOgg converts an audio file to Opus format in an Ogg container.
func ConvertToOpusOgg(inputFile string, outputFile string, bitrate string, sampleRate int) (string, error) {
// Check if input file exists
if _, err := os.Stat(inputFile); os.IsNotExist(err) {
return "", fmt.Errorf("input file not found: %s", inputFile)
}
// If no output file is specified, replace extension with .ogg
if outputFile == "" {
ext := filepath.Ext(inputFile)
outputFile = strings.TrimSuffix(inputFile, ext) + ".ogg"
}
// Ensure output directory exists
outputDir := filepath.Dir(outputFile)
if outputDir != "." {
if err := os.MkdirAll(outputDir, 0755); err != nil {
return "", fmt.Errorf("failed to create output directory: %w", err)
}
}
// Build the ffmpeg command
cmd := exec.Command("ffmpeg",
"-i", inputFile,
"-c:a", "libopus",
"-b:a", bitrate,
"-ar", fmt.Sprintf("%d", sampleRate),
"-application", "voip",
"-vbr", "on",
"-compression_level", "10",
"-frame_duration", "60",
"-y",
outputFile,
)
// Run command and capture output for error reporting
output, err := cmd.CombinedOutput()
if err != nil {
return "", fmt.Errorf("failed to convert audio. ffmpeg error: %s (%w)", string(output), err)
}
return outputFile, nil
}
// ConvertToOpusOggTempWithParams converts audio to a temporary .ogg file.
func ConvertToOpusOggTempWithParams(inputFile string, bitrate string, sampleRate int) (string, error) {
// Create a temporary file
tempFile, err := os.CreateTemp("", "audio-*.ogg")
if err != nil {
return "", fmt.Errorf("failed to create temp file: %w", err)
}
tempPath := tempFile.Name()
err = tempFile.Close()
if err != nil {
return "", err
} // Close it so ffmpeg can write to the path
// Convert the audio
result, err := ConvertToOpusOgg(inputFile, tempPath, bitrate, sampleRate)
if err != nil {
// Clean up on failure
err := os.Remove(tempPath)
if err != nil {
return "", err
}
return "", err
}
return result, nil
}
+554
View File
@@ -0,0 +1,554 @@
package helpers
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"log"
"log/slog"
"net/http"
"path/filepath"
"strings"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
// InitMcpTool initializes MCP tool for the MCP server
func InitMcpTool() {
server := mcp.NewServer(&mcp.Implementation{
Name: "whatsapp-mcp",
Version: "v1.0.0",
}, nil)
mcp.AddTool[searchContactsInput, any](server, &mcp.Tool{
Name: "search_contacts",
Description: "Search WhatsApp contacts by name or phone number.",
}, searchContactsHandler)
mcp.AddTool[listMessagesInput, any](server, &mcp.Tool{
Name: "list_messages",
Description: "Get WhatsApp messages matching specified criteria with optional context around matches.",
}, listMessagesHandler)
mcp.AddTool[getMessageContextInput, any](server, &mcp.Tool{
Name: "get_message_context",
Description: "Get surrounding messages (context) around a specific WhatsApp message.",
}, getMessageContextHandler)
mcp.AddTool[listChatsInput, any](server, &mcp.Tool{
Name: "list_chats",
Description: "Get list of WhatsApp chats, optionally filtered and sorted.",
}, listChatsHandler)
mcp.AddTool[getChatInput, any](server, &mcp.Tool{
Name: "get_chat",
Description: "Get metadata of a specific WhatsApp chat by JID.",
}, getChatHandler)
mcp.AddTool[getDirectChatByContactInput, any](server, &mcp.Tool{
Name: "get_direct_chat_by_contact",
Description: "Get direct (1:1) chat metadata by contact phone number.",
}, getDirectChatByContactHandler)
mcp.AddTool[getContactChatsInput, any](server, &mcp.Tool{
Name: "get_contact_chats",
Description: "Get all chats involving a specific contact (JID).",
}, getContactChatsHandler)
mcp.AddTool[getLastInteractionInput, any](server, &mcp.Tool{
Name: "get_last_interaction",
Description: "Get most recent WhatsApp message involving the contact.",
}, getLastInteractionHandler)
mcp.AddTool[sendMessageInput, map[string]any](server, &mcp.Tool{
Name: "send_message",
Description: "Send a text message to a person or group on WhatsApp. For groups use the group JID.",
}, sendMessageHandler)
mcp.AddTool[sendFileInput, map[string]any](server, &mcp.Tool{
Name: "send_file",
Description: "Send image, video, document or any file via WhatsApp.",
}, sendFileHandler)
mcp.AddTool[sendAudioMessageInput, map[string]any](server, &mcp.Tool{
Name: "send_audio_message",
Description: "Send audio/voice message (converted to Opus .ogg if needed).",
}, sendAudioMessageHandler)
mcp.AddTool[downloadMediaInput, map[string]any](server, &mcp.Tool{
Name: "download_media",
Description: "Download media from a WhatsApp message and return local file path.",
}, downloadMediaHandler)
isSSE := strings.ToLower(ReadEnv("IS_SSE", "false")) == "true" ||
strings.ToLower(ReadEnv("IS_SSE", "0")) == "1"
ctx := context.Background()
if isSSE {
addr := ReadEnv("SSE_BASE_URL", "0.0.0.0:5777")
slog.Info("Starting WhatsApp MCP HTTP server (SSE)", "addr", addr)
handler := mcp.NewSSEHandler(func(request *http.Request) *mcp.Server {
// A logic can be added here to return different servers based on URL path
// For now, it returns the same WhatsApp server for the root/default path
return server
}, nil)
if err := http.ListenAndServe(addr, handler); err != nil {
log.Fatalf("http server failed: %v", err)
}
} else {
slog.Info("Starting WhatsApp MCP server in stdio mode")
if err := server.Run(ctx, &mcp.StdioTransport{}); err != nil {
log.Fatalf("stdio failed: %v", err)
}
}
}
type searchContactsInput struct {
Query string `json:"query" mcp:"description:Search term to match against contact names or phone numbers"`
}
type listMessagesInput struct {
After *string `mcp:"description:ISO-8601 formatted string"`
Before *string `json:"before,omitempty" jsonschema:"description:ISO-8601 formatted string"`
SenderPhoneNumber *string `json:"sender_phone_number,omitempty"`
ChatJid *string `json:"chat_jid,omitempty"`
Query *string `json:"query,omitempty" jsonschema:"description:Search term in message content"`
Limit int `json:"limit" jsonschema:"default:20"`
Page int `json:"page" jsonschema:"default:0"`
IncludeContext bool `json:"include_context" jsonschema:"default:true"`
ContextBefore int `json:"context_before" jsonschema:"default:1"`
ContextAfter int `json:"context_after" jsonschema:"default:1"`
}
type getMessageContextInput struct {
MessageID string `json:"message_id" jsonschema:"description:The ID of the message"`
Before int `json:"before" jsonschema:"default:5"`
After int `json:"after" jsonschema:"default:5"`
}
type listChatsInput struct {
Query *string `json:"query,omitempty"`
Limit int `json:"limit" jsonschema:"default:20"`
Page int `json:"page" jsonschema:"default:0"`
IncludeLastMessage bool `json:"include_last_message" jsonschema:"default:true"`
SortBy string `json:"sort_by" jsonschema:"default:last_active,enum:last_active|name"`
}
type getChatInput struct {
ChatJid string `json:"chat_jid" jsonschema:"description:The JID of the chat"`
IncludeLastMessage bool `json:"include_last_message" jsonschema:"default:true"`
}
type getDirectChatByContactInput struct {
SenderPhoneNumber string `json:"sender_phone_number" jsonschema:"description:Phone number with country code"`
}
type getContactChatsInput struct {
Jid string `json:"jid"`
Limit int `json:"limit" jsonschema:"default:20"`
Page int `json:"page" jsonschema:"default:0"`
}
type getLastInteractionInput struct {
Jid string `json:"jid" jsonschema:"description:The contact's JID"`
}
type sendMessageInput struct {
Recipient string `json:"recipient" jsonschema:"description:Phone number (no +) or group JID like 123@g.us"`
Message string `json:"message"`
}
type sendFileInput struct {
Recipient string `json:"recipient"`
MediaPath string `json:"media_path" jsonschema:"description:Absolute path to the file"`
}
type sendAudioMessageInput struct {
Recipient string `json:"recipient"`
MediaPath string `json:"media_path" jsonschema:"description:Absolute path to audio file"`
}
type downloadMediaInput struct {
MessageID string `json:"message_id"`
ChatJid string `json:"chat_jid"`
}
func callAPI(method, path string, body any) ([]byte, error) {
var reqBody io.Reader
if body != nil {
b, err := json.Marshal(body)
if err != nil {
return nil, err
}
reqBody = bytes.NewReader(b)
}
req, err := http.NewRequest(method, apiBaseURL+path, reqBody)
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
client := &http.Client{Timeout: apiTimeout}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
data, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if resp.StatusCode >= 400 {
return nil, fmt.Errorf("API error %d: %s", resp.StatusCode, string(data))
}
return data, nil
}
func sendMessageHandler(
ctx context.Context,
req *mcp.CallToolRequest,
in sendMessageInput,
) (*mcp.CallToolResult, map[string]any, error) {
if in.Recipient == "" || in.Message == "" {
return &mcp.CallToolResult{
IsError: true,
Content: []mcp.Content{
&mcp.TextContent{Text: "recipient and message are required"},
},
}, map[string]any{
"success": false,
"error": "recipient and message are required",
}, nil
}
payload := map[string]any{
"recipient": in.Recipient,
"message": in.Message,
}
data, err := callAPI(http.MethodPost, "/send", payload)
if err != nil {
return &mcp.CallToolResult{
IsError: true,
Content: []mcp.Content{
&mcp.TextContent{Text: err.Error()},
},
}, map[string]any{
"success": false,
"error": err.Error(),
}, nil
}
var resp map[string]any
if err := json.Unmarshal(data, &resp); err != nil {
return &mcp.CallToolResult{
IsError: true,
Content: []mcp.Content{
&mcp.TextContent{Text: "failed to parse API response"},
},
}, map[string]any{
"success": false,
"error": "failed to parse API response",
}, nil
}
return &mcp.CallToolResult{}, resp, nil
}
func searchContactsHandler(
ctx context.Context,
req *mcp.CallToolRequest,
in searchContactsInput,
) (*mcp.CallToolResult, any, error) {
if in.Query == "" {
return ErrResult("query is required"), nil, nil
}
data, err := callAPI(http.MethodGet, "/contacts/search?q="+in.Query, nil)
if err != nil {
return ErrResult(err.Error()), nil, nil
}
var result struct {
Contacts []map[string]any `json:"contacts"`
}
if err := json.Unmarshal(data, &result); err != nil {
return ErrResult("invalid response format"), nil, nil
}
return OkResult(result.Contacts), nil, nil
}
// list_messages (similar for all read/list tools that return slices/maps)
func listMessagesHandler(
ctx context.Context,
req *mcp.CallToolRequest,
in listMessagesInput,
) (*mcp.CallToolResult, any, error) {
q := ""
if in.After != nil {
q += "&after=" + *in.After
}
if in.Before != nil {
q += "&before=" + *in.Before
}
if in.SenderPhoneNumber != nil {
q += "&sender=" + *in.SenderPhoneNumber
}
if in.ChatJid != nil {
q += "&chat=" + *in.ChatJid
}
if in.Query != nil {
q += "&search=" + *in.Query
}
q += fmt.Sprintf("&limit=%d&page=%d", in.Limit, in.Page)
if in.IncludeContext {
q += "&context=true"
}
data, err := callAPI(http.MethodGet, "/messages?"+strings.TrimPrefix(q, "&"), nil)
if err != nil {
return ErrResult(err.Error()), nil, nil
}
// The /messages endpoint returns plain text → we return it as string
return OkResult(string(data)), nil, nil
}
// download_media (same idea)
func downloadMediaHandler(
ctx context.Context,
req *mcp.CallToolRequest,
in downloadMediaInput,
) (*mcp.CallToolResult, map[string]any, error) {
path, err := DownloadMedia(in.MessageID, in.ChatJid)
if err != nil || path == "" {
msg := "failed to download media"
if err != nil {
msg += ": " + err.Error()
}
return &mcp.CallToolResult{}, map[string]any{
"success": false,
"message": msg,
}, nil
}
return &mcp.CallToolResult{}, map[string]any{
"success": true,
"message": "Media downloaded successfully",
"file_path": path,
}, nil
}
func getMessageContextHandler(
ctx context.Context,
req *mcp.CallToolRequest,
in getMessageContextInput,
) (*mcp.CallToolResult, any, error) {
if in.MessageID == "" {
return ErrResult("message_id is required"), nil, nil
}
path := fmt.Sprintf("/messages/context/%s?before=%d&after=%d",
in.MessageID, in.Before, in.After)
data, err := callAPI(http.MethodGet, path, nil)
if err != nil {
return ErrResult(err.Error()), nil, nil
}
var ctxData map[string]any
if err := json.Unmarshal(data, &ctxData); err != nil {
return ErrResult("invalid context response"), nil, nil
}
return OkResult(ctxData), nil, nil
}
func listChatsHandler(
ctx context.Context,
req *mcp.CallToolRequest,
in listChatsInput,
) (*mcp.CallToolResult, any, error) {
q := fmt.Sprintf("?limit=%d&page=%d", in.Limit, in.Page)
if in.Query != nil && *in.Query != "" {
q += "&q=" + *in.Query
}
if in.SortBy != "" {
q += "&sort=" + in.SortBy
}
data, err := callAPI(http.MethodGet, "/chats"+q, nil)
if err != nil {
return ErrResult(err.Error()), nil, nil
}
var result struct {
Chats []map[string]any `json:"chats"`
Count int `json:"count"`
}
_ = json.Unmarshal(data, &result) // best effort
return OkResult(result.Chats), nil, nil
}
func getChatHandler(
ctx context.Context,
req *mcp.CallToolRequest,
in getChatInput,
) (*mcp.CallToolResult, any, error) {
if in.ChatJid == "" {
return ErrResult("chat_jid is required"), nil, nil
}
data, err := callAPI(http.MethodGet, "/chats/"+in.ChatJid, nil)
if err != nil {
return ErrResult(err.Error()), nil, nil
}
var result map[string]any
if err := json.Unmarshal(data, &result); err != nil {
return ErrResult("invalid chat response"), nil, nil
}
return OkResult(result["chat"]), nil, nil
}
func getDirectChatByContactHandler(
ctx context.Context,
req *mcp.CallToolRequest,
in getDirectChatByContactInput,
) (*mcp.CallToolResult, any, error) {
if in.SenderPhoneNumber == "" {
return ErrResult("sender_phone_number is required"), nil, nil
}
// GET /api/direct-contacts/{phone}/chat
path := fmt.Sprintf("/direct-contacts/%s/chat", in.SenderPhoneNumber)
data, err := callAPI(http.MethodGet, path, nil)
if err != nil {
if strings.Contains(err.Error(), "404") {
return &mcp.CallToolResult{
IsError: true,
Content: []mcp.Content{
&mcp.TextContent{Text: "No direct (1:1) chat found for this phone number"},
},
}, nil, nil
}
return ErrResult(err.Error()), nil, nil
}
var result map[string]any
if err := json.Unmarshal(data, &result); err != nil {
return ErrResult("failed to parse chat response"), nil, nil
}
chat, ok := result["chat"]
if !ok {
return ErrResult("chat object missing in response"), nil, nil
}
return &mcp.CallToolResult{}, chat, nil
}
func getContactChatsHandler(
ctx context.Context,
req *mcp.CallToolRequest,
in getContactChatsInput,
) (*mcp.CallToolResult, any, error) {
if in.Jid == "" {
return ErrResult("jid is required"), nil, nil
}
limit := in.Limit
if limit <= 0 {
limit = 20
}
page := in.Page
if page < 0 {
page = 0
}
// GET /api/contacts/{jid}/chats?limit=...&page=...
path := fmt.Sprintf("/contacts/%s/chats?limit=%d&page=%d", in.Jid, limit, page)
data, err := callAPI(http.MethodGet, path, nil)
if err != nil {
return ErrResult(err.Error()), nil, nil
}
var result struct {
Chats []map[string]any `json:"chats"`
Count int `json:"count"`
}
if err := json.Unmarshal(data, &result); err != nil {
return ErrResult("failed to parse chats response"), nil, nil
}
return &mcp.CallToolResult{}, result.Chats, nil
}
func getLastInteractionHandler(
ctx context.Context,
req *mcp.CallToolRequest,
in getLastInteractionInput,
) (*mcp.CallToolResult, any, error) {
if in.Jid == "" {
return ErrResult("jid is required"), nil, nil
}
// We simulate it by asking for 1 message from that sender
data, err := callAPI(http.MethodGet, "/messages?sender="+in.Jid+"&limit=1", nil)
if err != nil {
return ErrResult(err.Error()), nil, nil
}
return OkResult(string(data)), nil, nil
}
func sendFileHandler(ctx context.Context,
req *mcp.CallToolRequest,
in sendFileInput) (*mcp.CallToolResult, map[string]any, error) {
if in.MediaPath == "" {
return &mcp.CallToolResult{
IsError: true,
Content: []mcp.Content{&mcp.TextContent{Text: "media_path is required"}},
}, nil, nil
}
absPath, err := filepath.Abs(in.MediaPath)
if err != nil {
return &mcp.CallToolResult{
IsError: true,
Content: []mcp.Content{&mcp.TextContent{Text: fmt.Sprintf("invalid path: %v", err)}},
}, nil, nil
}
success, msg := SendFile(in.Recipient, absPath)
resultData := map[string]any{"success": success, "message": msg}
return &mcp.CallToolResult{IsError: !success}, resultData, nil
}
func sendAudioMessageHandler(ctx context.Context,
req *mcp.CallToolRequest,
in sendAudioMessageInput) (*mcp.CallToolResult, map[string]any, error) {
success, msg := SendAudioVoiceMessage(in.Recipient, in.MediaPath)
resultData := map[string]any{"success": success, "message": msg}
return &mcp.CallToolResult{IsError: !success}, resultData, nil
}
+51
View File
@@ -0,0 +1,51 @@
package helpers
import (
"encoding/json"
"os"
"time"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
var apiBaseURL = ReadEnv("API_BASE_URL", "http://192.168.178.119:30015/api")
const apiTimeout = 25 * time.Second
// OkResult return proper ok result for mcp tool
func OkResult(v any) *mcp.CallToolResult {
b, err := json.Marshal(v)
if err != nil {
return &mcp.CallToolResult{
IsError: true,
Content: []mcp.Content{
&mcp.TextContent{Text: "Failed to encode JSON: " + err.Error()},
},
}
}
return &mcp.CallToolResult{
IsError: false,
Content: []mcp.Content{
&mcp.TextContent{Text: string(b)},
},
}
}
// ErrResult return error result for mcp tool
func ErrResult(msg string) *mcp.CallToolResult {
return &mcp.CallToolResult{
IsError: true,
Content: []mcp.Content{
&mcp.TextContent{Text: msg},
},
}
}
// ReadEnv read return value for an env
func ReadEnv(key, fallback string) string {
if value, ok := os.LookupEnv(key); ok {
return value
}
return fallback
}
+254
View File
@@ -0,0 +1,254 @@
package helpers
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"net/http"
"os"
"strings"
"time"
_ "github.com/mattn/go-sqlite3"
)
type Message struct {
Timestamp time.Time `json:"timestamp"`
Sender string `json:"sender"`
Content string `json:"content"`
IsFromMe bool `json:"is_from_me"`
ChatJID string `json:"chat_jid"`
ID string `json:"id"`
ChatName string `json:"chat_name,omitempty"`
MediaType string `json:"media_type,omitempty"`
}
type Chat struct {
JID string `json:"jid"`
Name string `json:"name,omitempty"`
LastMessageTime time.Time `json:"last_message_time,omitempty"`
LastMessage string `json:"last_message,omitempty"`
LastSender string `json:"last_sender,omitempty"`
LastIsFromMe bool `json:"last_is_from_me,omitempty"`
}
func (c *Chat) IsGroup() bool {
return strings.HasSuffix(c.JID, "@g.us")
}
type Contact struct {
PhoneNumber string `json:"phone_number"`
Name string `json:"name,omitempty"`
JID string `json:"jid"`
}
type MessageContext struct {
Message Message `json:"message"`
Before []Message `json:"before"`
After []Message `json:"after"`
}
type ListMessagesParams struct {
After, Before string
SenderPhoneNumber *string
ChatJid *string
Query *string
Limit, Page int
IncludeContext bool
ContextBefore int
ContextAfter int
}
func SendMessage(recipient, message string) (bool, string) {
if recipient == "" {
return false, "Recipient must be provided"
}
payload := map[string]string{
"recipient": recipient,
"message": message,
}
body, _ := json.Marshal(payload)
req, _ := http.NewRequest("POST", apiBaseURL+"/send", bytes.NewBuffer(body))
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return false, "Request error: " + err.Error()
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return false, fmt.Sprintf("HTTP %d - %s", resp.StatusCode, string(body))
}
var result map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return false, "Failed to parse response"
}
success, _ := result["success"].(bool)
msg, _ := result["message"].(string)
if msg == "" {
msg = "Unknown response"
}
return success, msg
}
func SendFile(recipient, mediaPath string) (bool, string) {
if recipient == "" {
return false, "Recipient must be provided"
}
if mediaPath == "" {
return false, "Media path must be provided"
}
if _, err := os.Stat(mediaPath); os.IsNotExist(err) {
return false, "Media file not found: " + mediaPath
}
payload := map[string]string{
"recipient": recipient,
"media_path": mediaPath,
}
body, _ := json.Marshal(payload)
req, _ := http.NewRequest("POST", apiBaseURL+"/send", bytes.NewBuffer(body))
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return false, "Request error: " + err.Error()
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return false, fmt.Sprintf("HTTP %d - %s", resp.StatusCode, string(body))
}
var result map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return false, "Failed to parse response"
}
success, _ := result["success"].(bool)
msg, _ := result["message"].(string)
if msg == "" {
msg = "Unknown response"
}
return success, msg
}
func SendAudioVoiceMessage(recipient, mediaPath string) (bool, string) {
if recipient == "" {
return false, "Recipient must be provided"
}
if mediaPath == "" {
return false, "Media path must be provided"
}
if _, err := os.Stat(mediaPath); os.IsNotExist(err) {
return false, "Media file not found: " + mediaPath
}
finalPath := mediaPath
if !strings.HasSuffix(strings.ToLower(mediaPath), ".ogg") {
converted, err := ConvertToOpusOggTemp(mediaPath)
if err != nil {
return false, "Audio conversion failed (ffmpeg required?): " + err.Error()
}
finalPath = converted
defer func(name string) {
err := os.Remove(name)
if err != nil {
}
}(finalPath)
}
payload := map[string]string{
"recipient": recipient,
"media_path": finalPath,
}
body, _ := json.Marshal(payload)
req, _ := http.NewRequest("POST", apiBaseURL+"/send", bytes.NewBuffer(body))
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return false, "Request error: " + err.Error()
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return false, fmt.Sprintf("HTTP %d - %s", resp.StatusCode, string(body))
}
var result map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return false, "Failed to parse response"
}
success, _ := result["success"].(bool)
msg, _ := result["message"].(string)
if msg == "" {
msg = "Unknown response"
}
return success, msg
}
func DownloadMedia(messageID, chatJID string) (string, error) {
payload := map[string]string{
"message_id": messageID,
"chat_jid": chatJID,
}
body, _ := json.Marshal(payload)
req, _ := http.NewRequest("POST", apiBaseURL+"/download", bytes.NewBuffer(body))
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return "", fmt.Errorf("HTTP %d - %s", resp.StatusCode, string(body))
}
var result map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return "", err
}
if success, ok := result["success"].(bool); !ok || !success {
msg, _ := result["message"].(string)
if msg == "" {
msg = "Unknown error"
}
return "", errors.New(msg)
}
path, ok := result["path"].(string)
if !ok || path == "" {
return "", errors.New("no path returned")
}
log.Printf("Media downloaded: %s", path)
return path, nil
}
+7
View File
@@ -0,0 +1,7 @@
package main
import "whatsapp-mcp-server/helpers"
func main() {
helpers.InitMcpTool()
}