wa-mcp
Provides WhatsApp management capabilities, including session lifecycle handling via Baileys, QR and phone-number onboarding, message storage, media download, and optional voice note transcription through whisper.cpp.
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@wa-mcpcheck the QR code status for my WhatsApp session"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
wa-mcp
Self-hosted WhatsApp management over Model Context Protocol. One Docker stack per client. Baileys handles WhatsApp; SQLite stores messages; local disk stores originals; optional whisper.cpp transcribes voice notes on CPU.
Current scope: local development and validation. No VPS, tunnel, WAF, registry image, or public service has been deployed.
Unofficial WhatsApp integration. Baileys is not affiliated with WhatsApp or Meta. Linked accounts can be rate-limited or banned. Use a test account first.
Architecture
Component | Purpose |
OpenWA v0.13.0 baseline | NestJS API, Baileys session lifecycle, QR onboarding, SQLite, dashboard |
Streamable HTTP MCP | Stateless |
SQLite | Sessions, messages, raw Baileys message references, media metadata, transcripts |
Local media volume | Original documents/audio plus visual media downloaded on command |
| Optional internal-only CPU server using multilingual |
OAuth facade | Static client, PKCE, refresh tokens, callback allowlist; no external auth portal |
Persistent Docker volumes:
wa-data: SQLite databases, Baileys auth state, saved media.whisper-models: verifiedggml-medium.binmodel.
Related MCP server: wa-bridge
Requirements
Docker Desktop with Linux containers, or Docker Engine + Compose on Linux.
At least 4 GB RAM when transcription runs. Medium model requires roughly 2.1 GB working memory plus the API.
About 2 GB free model storage, plus media/database capacity.
A secondary WhatsApp account for first tests.
Validated development target: Docker 29.3.0, Compose 5.1.0, Linux amd64.
Local setup
Copy the environment template:
Copy-Item .env.example .envGenerate secrets in PowerShell:
$clientBytes = [Security.Cryptography.RandomNumberGenerator]::GetBytes(16)
$secretBytes = [Security.Cryptography.RandomNumberGenerator]::GetBytes(32)
[Convert]::ToHexString($clientBytes).ToLower()
[Convert]::ToHexString($secretBytes).ToLower()Put the first value in MCP_CLIENT_ID; put the second in MCP_CLIENT_SECRET. Never commit .env.
Start without transcription:
docker compose up -d --build
docker compose psStart with transcription:
# Set VOICE_TRANSCRIPTION_ENABLED=true in .env first.
docker compose --profile transcription up -d --build
docker compose --profile transcription psFirst transcription start downloads the full multilingual medium model, verifies this SHA-256, then starts the server:
6c14d5adee5f86394037b4e4e8b59f1673b6cee10e3cf0b11bbdbee79c156208The model and container images are not committed to Git. Dockerfile.whisper rebuilds the pinned
official source with GGML_NATIVE=OFF. This avoids illegal-instruction crashes when a client VPS CPU
does not expose instruction sets available on the upstream image's build machine.
QR onboarding
Open
http://127.0.0.1:2785.Use
MCP_CLIENT_SECRETas the bootstrap API key.Create a session. Use a stable name such as
client-main.Start the session.
Open its QR view.
On the phone: WhatsApp → Linked devices → Link a device → scan.
Wait for session status
ready.
PowerShell REST equivalent. .env does not populate the current PowerShell process, so enter the same MCP_CLIENT_SECRET when prompted:
$base = 'http://127.0.0.1:2785'
$secureApiKey = Read-Host 'Enter MCP_CLIENT_SECRET' -AsSecureString
$apiKey = [Net.NetworkCredential]::new('', $secureApiKey).Password
$headers = @{ 'X-API-Key' = $apiKey }
$session = Invoke-RestMethod `
-Method Post `
-Uri "$base/api/sessions" `
-Headers $headers `
-ContentType 'application/json' `
-Body '{"name":"client-main"}'
$sessionId = $session.id
Invoke-RestMethod `
-Method Post `
-Uri "$base/api/sessions/$sessionId/start" `
-Headers $headersSession creation returns 409 when that name already exists. List sessions and reuse the intended ID instead of creating a duplicate:
Invoke-RestMethod -Uri "$base/api/sessions" -Headers $headersStarting is asynchronous. Check the session first:
$sessionState = Invoke-RestMethod `
-Uri "$base/api/sessions/$sessionId" `
-Headers $headers
$sessionStateIf status is already ready, skip QR retrieval. Otherwise poll for the QR for up to two minutes. HTTP 400 means the engine has not produced it yet:
$qr = $null
$qrDeadline = (Get-Date).AddMinutes(2)
do {
try {
$qr = Invoke-RestMethod `
-Uri "$base/api/sessions/$sessionId/qr" `
-Headers $headers
} catch {
$statusCode = if ($_.Exception.Response) {
[int]$_.Exception.Response.StatusCode
} else {
0
}
if ($statusCode -ne 400) { throw }
Start-Sleep -Seconds 2
}
} while (-not $qr -and (Get-Date) -lt $qrDeadline)
if (-not $qr) { throw 'QR was not ready within two minutes. Check API logs.' }The response contains qrCode as a PNG data URL. Save and open it:
$qrBytes = [Convert]::FromBase64String(
($qr.qrCode -replace '^data:image/png;base64,', '')
)
$qrPath = Join-Path (Get-Location) 'whatsapp-qr.png'
[IO.File]::WriteAllBytes($qrPath, $qrBytes)
Start-Process $qrPathScan it on the phone. Then wait for ready:
$readyDeadline = (Get-Date).AddMinutes(2)
do {
$sessionState = Invoke-RestMethod `
-Uri "$base/api/sessions/$sessionId" `
-Headers $headers
if ($sessionState.status -in @('failed', 'action_required')) {
throw "WhatsApp session entered $($sessionState.status): $($sessionState.lastError)"
}
if ($sessionState.status -ne 'ready') { Start-Sleep -Seconds 2 }
} while ($sessionState.status -ne 'ready' -and (Get-Date) -lt $readyDeadline)
if ($sessionState.status -ne 'ready') {
throw "Session did not become ready. Last status: $($sessionState.status)"
}
$sessionState
Remove-Item -LiteralPath $qrPathThe QR file carries temporary pairing material. Delete it after a successful scan, as shown above.
Phone-number pairing alternative
Start the session first. Send digits only: country code plus number, without +, spaces, or dashes.
$pairingBody = @{ phoneNumber = '919876543210' } | ConvertTo-Json
$pairing = Invoke-RestMethod `
-Method Post `
-Uri "$base/api/sessions/$sessionId/pairing-code" `
-Headers $headers `
-ContentType 'application/json' `
-Body $pairingBody
$pairing.pairingCodeOn the phone, choose WhatsApp's phone-number linking option and enter the returned eight-character code. Poll the session endpoint until status becomes ready.
Baileys auth persists under the wa-data volume. Ordinary container restarts do not require another scan.
Session operations
These commands reuse $base, $headers, and $sessionId from QR onboarding.
List sessions and inspect one session:
Invoke-RestMethod -Uri "$base/api/sessions" -Headers $headers
Invoke-RestMethod -Uri "$base/api/sessions/$sessionId" -Headers $headersStop the live engine without unlinking WhatsApp, then start it again:
Invoke-RestMethod -Method Post -Uri "$base/api/sessions/$sessionId/stop" -Headers $headers
Invoke-RestMethod -Method Post -Uri "$base/api/sessions/$sessionId/start" -Headers $headersForce-kill only a wedged engine. Stored WhatsApp credentials remain available for restart:
Invoke-RestMethod -Method Post -Uri "$base/api/sessions/$sessionId/force-kill" -Headers $headersLogout is destructive: it unlinks the companion device and removes stored WhatsApp credentials. Next start requires fresh pairing:
Invoke-RestMethod -Method Post -Uri "$base/api/sessions/$sessionId/logout" -Headers $headersDelete is destructive: it removes the session record and its local credentials. Confirm the exact UUID first:
Invoke-RestMethod -Uri "$base/api/sessions/$sessionId" -Headers $headers
Invoke-RestMethod -Method Delete -Uri "$base/api/sessions/$sessionId" -Headers $headersMCP endpoint and auth
Endpoint:
POST http://127.0.0.1:2785/mcpSupported auth:
OAuth authorization-code flow with PKCE and refresh tokens. Intended for ChatGPT/Claude remote connectors.
HTTP Basic: username
MCP_CLIENT_ID, passwordMCP_CLIENT_SECRET.Bearer or
X-API-Key: raw API key, useful for Inspector and other MCP clients.
OAuth metadata:
/.well-known/oauth-protected-resource
/.well-known/oauth-protected-resource/mcp
/.well-known/oauth-authorization-server
/authorize
/tokenMCP_REDIRECT_URIS is a comma-separated exact allowlist. Unknown callback URLs fail closed. Claude's documented callback is included in .env.example; add the callback shown by ChatGPT during connector setup before deployment.
Basic-auth handshake example. Enter values from .env; do not paste them into committed scripts:
$mcpClientId = Read-Host 'Enter MCP_CLIENT_ID'
$secureMcpClientSecret = Read-Host 'Enter MCP_CLIENT_SECRET' -AsSecureString
$mcpClientSecret = [Net.NetworkCredential]::new('', $secureMcpClientSecret).Password
$pair = "${mcpClientId}:${mcpClientSecret}"
$basic = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($pair))
$mcpHeaders = @{
Authorization = "Basic $basic"
Accept = 'application/json, text/event-stream'
}
$initializeBody = @{
jsonrpc = '2.0'
id = 1
method = 'initialize'
params = @{
protocolVersion = '2025-11-25'
capabilities = @{}
clientInfo = @{ name = 'manual-test'; version = '1.0.0' }
}
} | ConvertTo-Json -Depth 8
$initializeResponse = Invoke-WebRequest `
-UseBasicParsing `
-Method Post `
-Uri 'http://127.0.0.1:2785/mcp' `
-Headers $mcpHeaders `
-ContentType 'application/json' `
-Body $initializeBody
$initializeResponse.StatusCode
$initializeResponse.ContentExpected HTTP status: 200 or 202. Streamable HTTP may return JSON or text/event-stream.
Confirm tool discovery:
$toolsBody = @{
jsonrpc = '2.0'
id = 2
method = 'tools/list'
params = @{}
} | ConvertTo-Json -Depth 5
$toolsResponse = Invoke-WebRequest `
-UseBasicParsing `
-Method Post `
-Uri 'http://127.0.0.1:2785/mcp' `
-Headers $mcpHeaders `
-ContentType 'application/json' `
-Body $toolsBody
$toolsResponse.StatusCode
$toolsResponse.ContentChatGPT custom MCP apps currently require a remote endpoint; they do not connect directly to localhost. Full write-capable custom MCP apps also depend on eligible ChatGPT workspace plans and admin settings. Claude remote connectors likewise connect from Anthropic infrastructure and support unauthenticated or OAuth servers—not arbitrary Basic auth. Client ID/secret fields in Claude are OAuth client credentials.
MCP tools
Every tool has one job. Every input is schema-validated. All tools require sessionId.
Chats and messages
list_chatsget_chatlist_unread_chatslist_messagessearch_messagesget_message_context
Contacts and groups
search_contactsget_contactget_grouplist_group_participants
Actions
send_text_messagesend_group_message_with_mentionsreply_to_messageforward_messagereact_to_messagesend_saved_media
Media
get_media_metadatalist_saved_mediadownload_mediaread_saved_media
read_saved_media returns bounded base64 chunks, maximum 1 MiB per call. It never returns an unbounded file in one MCP response.
Voice
get_voice_transcriptget_transcription_statusretry_voice_transcription
Metrics
count_inbound_messagescount_outbound_messagescount_active_chatslist_top_active_chatslist_unanswered_chatsget_median_first_response_timeget_p90_first_response_time
No generic executor. No combined business-metrics tool.
Example tool call:
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "list_unanswered_chats",
"arguments": {
"sessionId": "SESSION_UUID",
"from": "2026-08-01T00:00:00.000Z",
"limit": 20
}
}
}Media policy
Incoming type | Default behavior |
Document | Auto-download original |
Audio/voice note | Auto-download original |
Image | Metadata only; |
Video/GIF | Metadata only; |
Sticker | Metadata only; |
Saved originals use deterministic per-session paths. Each file is written through a temporary file, atomically linked, and stored with byte size plus SHA-256. Duplicate message events converge on one database row and one file.
Visual on-demand downloads depend on the persisted Baileys raw-message store. Increase BAILEYS_MESSAGE_STORE_LIMIT if downloads must remain available far back in history.
Voice transcription
Configuration:
VOICE_TRANSCRIPTION_ENABLED=false
WHISPER_MODEL=medium
WHISPER_LANGUAGE=auto
WHISPER_THREADS=4
AUTO_DOWNLOAD_AUDIO=trueWhen disabled, voice media still saves and transcript status becomes disabled. No external transcription service runs. When enabled, the internal whisper.cpp server receives the saved original. No cloud fallback exists.
Stored transcript fields:
text
detected language
model
start/completion timestamps
audio duration
linked media and WhatsApp message IDs
pending,processing,success,error, ordisabledstatusbounded error text
Pending/processing jobs resume after restart. Failed jobs remain errors until retry_voice_transcription is called.
Logs and health commands
Show container state and one-shot resource usage:
docker compose ps
docker stats --no-streamCheck application readiness:
$health = Invoke-RestMethod 'http://127.0.0.1:2785/api/health/ready'
$healthRead recent API logs or follow new entries:
docker compose logs --tail 200 api
docker compose logs -f apiRead transcription startup, model-download, checksum, and server logs:
docker compose --profile transcription logs --tail 200 whisper-model whisper
docker compose --profile transcription logs -f whisperStop log following with Ctrl+C; containers continue running.
Restart only the API, then recheck readiness and the persisted WhatsApp session:
docker compose restart api
Invoke-RestMethod 'http://127.0.0.1:2785/api/health/ready'
Invoke-RestMethod -Uri "$base/api/sessions/$sessionId" -Headers $headersStop or start the complete stack without deleting volumes:
docker compose stop
docker compose startdocker compose down removes containers and networks but keeps named volumes unless --volumes is supplied. Never add --volumes during ordinary maintenance.
First-install verification
Run this sequence before connecting ChatGPT, Claude, or a production phone number:
Validate Compose interpolation:
$env:MCP_CLIENT_ID = 'compose-check-client' $env:MCP_CLIENT_SECRET = 'replace-with-a-temporary-32-byte-or-longer-secret' docker compose config --quiet Remove-Item Env:MCP_CLIENT_ID Remove-Item Env:MCP_CLIENT_SECRETStart the selected profile and confirm every required service is healthy:
docker compose up -d --build docker compose ps Invoke-RestMethod 'http://127.0.0.1:2785/api/health/ready'For transcription-enabled clients, use:
docker compose --profile transcription up -d --build docker compose --profile transcription psCreate and pair a secondary WhatsApp account using the QR or phone-number commands above. Require session status
ready.Run MCP initialize and
tools/list. Require HTTP200or202for initialize and HTTP200for tool discovery.Send one inbound text, document, voice note, image, and short video from another account.
Confirm documents and audio save automatically. Confirm images and videos remain metadata-only until an explicit media-download tool call.
When transcription is enabled, wait for voice transcript status
success; verify text, detected language, model, duration, and linked message/media IDs. When disabled, require statusdisabledand no Whisper service.Review one read-only MCP result. Then test one write action against the secondary account after checking its exact chat and payload.
Restart the API and confirm session, messages, media, and transcript state persist:
docker compose restart api Invoke-RestMethod 'http://127.0.0.1:2785/api/health/ready' Invoke-RestMethod -Uri "$base/api/sessions/$sessionId" -Headers $headersRun the source, Inspector, and backup checks below. Do not treat deployment as complete while any check remains unverified.
Tests
Local source checks:
npm.cmd ci
npm.cmd run build
npm.cmd test -- --runInBandCompose validation:
$env:MCP_CLIENT_ID = 'compose-check-client'
$env:MCP_CLIENT_SECRET = 'replace-with-a-temporary-32-byte-or-longer-secret'
docker compose config --quiet
Remove-Item Env:MCP_CLIENT_ID
Remove-Item Env:MCP_CLIENT_SECRETMCP Inspector:
npx.cmd @modelcontextprotocol/inspectorSet the Inspector URL to http://127.0.0.1:2785/mcp; send Authorization: Basic <base64(client:secret)> or X-API-Key: <secret>.
GitHub Actions run Linux build, unit tests, Compose validation, API image build, portable Whisper image build, and a CPU server smoke test. CI uses the tiny model only for speed; production Compose always downloads and verifies multilingual medium.
Backup and restore
Stop the stack before file-level backup:
docker compose down
New-Item -ItemType Directory -Force backups | Out-Null
docker run --rm -v wa-mcp_wa-data:/source:ro -v "${PWD}/backups:/backup" alpine:3.22 tar -czf /backup/wa-data.tar.gz -C /source .
docker compose up -dModel backup is optional; it can be re-downloaded and checksum-verified. wa-data is mandatory: it contains both SQLite files, WhatsApp pairing state, and media.
Restore is destructive. Verify the archive and target volume first:
docker compose down
docker run --rm -v wa-mcp_wa-data:/target -v "${PWD}/backups:/backup:ro" alpine:3.22 sh -c "rm -rf /target/* && tar -xzf /backup/wa-data.tar.gz -C /target"
docker compose up -dVPS and Cloudflare Tunnel — documentation only
No deployment is performed by this repository setup.
Use the Cloudflare Tunnel deployment runbook for the exact named-tunnel, DNS, environment, WAF, validation, and rollback sequence. The runbook uses a remotely managed tunnel and a host cloudflared service so the application remains reachable only at 127.0.0.1:2785 on the VPS.
Expected production shape:
Buy one Linux VPS per client.
Clone this repository and create a unique
.env.Bind the app to localhost only.
Follow the runbook to create one named tunnel per client and route one HTTPS hostname to
http://127.0.0.1:2785.Do not use Cloudflare's MCP portal.
Apply Cloudflare WAF source-IP allowlists for the AI provider ranges you use.
Keep OAuth enabled. WAF is defense in depth, not authentication.
Set
BASE_URLto the public HTTPS origin and exact provider callbacks inMCP_REDIRECT_URIS.Test metadata, OAuth, MCP initialization, read tools, then write tools.
Do not use a temporary quick tunnel for a client product. Do not expose port 2785 directly. Provider source ranges and connector requirements change; verify current official documentation during deployment.
Security
WhatsApp messages and downloaded files are untrusted input. They can contain prompt injection telling a model to call write tools. Require client-side confirmation for sends/forwards and review tool inputs.
OAuth is single-client, single-installation infrastructure. Rotate
MCP_CLIENT_SECRETto revoke every issued access/refresh token.MCP_REDIRECT_URISuses exact matching. Never use wildcards./mcprequires transport auth before tool discovery. Each tool call repeats API-key role and session-scope authorization.Keep
.env,data/, media, SQLite, Baileys sessions, model files, and backups out of Git.WAF allowlists do not replace app auth. NAT/provider IP changes can also break legitimate connector traffic.
Media and transcript content can contain personal or regulated data. Define retention, access, consent, and deletion policy per client.
Backups contain credentials and message content. Encrypt them outside this stack.
Report vulnerabilities privately through the repository's GitHub Security Advisory feature. Do not open a public issue containing secrets or message data.
Troubleshooting
Compose refuses missing variables
Populate MCP_CLIENT_ID and MCP_CLIENT_SECRET in .env. Empty secrets fail closed.
QR never appears
Check docker compose logs api. Confirm ENGINE_TYPE=baileys, outbound internet works, session is started, and no stale phone pairing is active.
Visual media cannot download
The raw Baileys reference may have aged out of BAILEYS_MESSAGE_STORE_LIMIT, or WhatsApp may no longer serve the original. Raise the limit before long retention is required.
Transcript stays error
Run docker compose --profile transcription ps. Confirm VOICE_TRANSCRIPTION_ENABLED=true, model initialization succeeded, and Whisper is healthy. Inspect get_transcription_status; then call retry_voice_transcription after fixing the cause.
Medium model checksum fails
Delete only the invalid model volume after confirming its exact Compose project/volume name, then restart the transcription profile. Never bypass the checksum.
Connector cannot reach localhost
Expected. ChatGPT and Claude remote connectors originate from their cloud infrastructure. Deployment requires a public HTTPS route or an officially supported secure tunnel.
Public source and licensing
MIT license. See LICENSE and NOTICE.
A public MIT repository cannot contain a protected source file that prevents copying. MIT explicitly permits reuse, modification, and redistribution. .gitignore protects secrets/runtime data only. Proprietary product logic must live in a private repository/package under a different license; do not rely on obscurity inside this public repository.
Attribution
OpenWA, pinned baseline v0.13.0.
Baileys, pinned package
7.0.0-rc13.whisper.cpp, pinned OCI digest in Compose.
WhatsApp and Meta are trademarks of their respective owners. No affiliation or endorsement.
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Servers
- Flicense-qualityCmaintenanceWhatsApp MCP server that exposes messaging, groups, contacts, and profile management as tools and resources for AI agents, supporting Baileys and Meta Cloud API.Last updated19
- Alicense-qualityAmaintenanceA self-hosted WhatsApp bridge that exposes a stdio MCP server with ~20 tools for reading conversations, sending messages, managing groups, contacts, and aliases, enabling AI agents to operate WhatsApp directly.Last updated2MIT
- Alicense-qualityBmaintenanceMCP server exposing WhatsApp Cloud API operations as tools for AI agents like Claude Code, Cursor, and Codex.Last updatedMIT
- Alicense-qualityBmaintenanceA self-hosted WhatsApp gateway that exposes messaging capabilities via MCP, allowing AI agents to send, receive, and manage WhatsApp messages through a single-command setup with SQLite storage.Last updated1MIT
Related MCP Connectors
A comprehensive Model Context Protocol (MCP) server that enables AI assistants to interact with yo…
Self-hosted MCP gateway: turn any API, database or MCP server into AI connectors — no code.
Phone, SMS & email for AI agents — one remote MCP endpoint, OAuth login, zero install.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/girik7/wa-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server