feat: whatsapp-mcp-go
This commit is contained in:
@@ -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.
|
||||
@@ -0,0 +1,263 @@
|
||||
# WhatsApp‑MCP (Go Edition)
|
||||
|
||||
A lightweight, WhatsApp MCP server and bridge — rewritten entirely in Go for simplicity, portability, and real‑world automation workflows.
|
||||
|
||||
This project is a re‑imagining 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 |
|
||||
|------------|----------|
|
||||
|  |  |
|
||||
|
||||
## 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
|
||||
- Production‑ready architecture
|
||||
|
||||
---
|
||||
|
||||
## Credits
|
||||
|
||||
This project stands on the shoulders of two major contributors:
|
||||
|
||||
### **Original Author: lharries**
|
||||
The initial idea, architecture, and implementation of WhatsApp‑MCP 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 single‑language 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 MCP‑based 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 |
@@ -0,0 +1,7 @@
|
||||
.idea
|
||||
.vscode
|
||||
manifests
|
||||
Dockerfile
|
||||
README.md
|
||||
Makefile
|
||||
store
|
||||
@@ -0,0 +1 @@
|
||||
media
|
||||
Generated
+10
@@ -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
|
||||
Generated
+25
@@ -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
@@ -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
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="db-tree-configuration">
|
||||
<option name="data" value="---------------------------------------- 1:0:4f6200ec-1eae-4549-9d93-2a328888b8d7 2:0:ae157347-e4f8-43bd-a8b5-8556d3489f79 " />
|
||||
</component>
|
||||
</project>
|
||||
Generated
+11
@@ -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>
|
||||
Generated
+6
@@ -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>
|
||||
Generated
+8
@@ -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>
|
||||
Generated
+6
@@ -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>
|
||||
Generated
+6
@@ -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
@@ -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>
|
||||
@@ -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"]
|
||||
@@ -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
|
||||
@@ -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:
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
Generated
+10
@@ -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/
|
||||
Generated
+11
@@ -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>
|
||||
Generated
+7
@@ -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>
|
||||
Generated
+8
@@ -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>
|
||||
Generated
+6
@@ -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>
|
||||
@@ -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
|
||||
)
|
||||
@@ -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=
|
||||
@@ -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 → 24k–32k 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package main
|
||||
|
||||
import "whatsapp-mcp-server/helpers"
|
||||
|
||||
func main() {
|
||||
helpers.InitMcpTool()
|
||||
}
|
||||
Reference in New Issue
Block a user