Skip to main content
Glama

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 POST /mcp transport with 30 focused tools

SQLite

Sessions, messages, raw Baileys message references, media metadata, transcripts

Local media volume

Original documents/audio plus visual media downloaded on command

whisper.cpp

Optional internal-only CPU server using multilingual ggml-medium.bin

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: verified ggml-medium.bin model.

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 .env

Generate 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 ps

Start with transcription:

# Set VOICE_TRANSCRIPTION_ENABLED=true in .env first.
docker compose --profile transcription up -d --build
docker compose --profile transcription ps

First transcription start downloads the full multilingual medium model, verifies this SHA-256, then starts the server:

6c14d5adee5f86394037b4e4e8b59f1673b6cee10e3cf0b11bbdbee79c156208

The 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

  1. Open http://127.0.0.1:2785.

  2. Use MCP_CLIENT_SECRET as the bootstrap API key.

  3. Create a session. Use a stable name such as client-main.

  4. Start the session.

  5. Open its QR view.

  6. On the phone: WhatsApp → Linked devices → Link a device → scan.

  7. 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 $headers

Session 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 $headers

Starting is asynchronous. Check the session first:

$sessionState = Invoke-RestMethod `
  -Uri "$base/api/sessions/$sessionId" `
  -Headers $headers

$sessionState

If 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 $qrPath

Scan 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 $qrPath

The 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.pairingCode

On 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 $headers

Stop 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 $headers

Force-kill only a wedged engine. Stored WhatsApp credentials remain available for restart:

Invoke-RestMethod -Method Post -Uri "$base/api/sessions/$sessionId/force-kill" -Headers $headers

Logout 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 $headers

Delete 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 $headers

MCP endpoint and auth

Endpoint:

POST http://127.0.0.1:2785/mcp

Supported auth:

  • OAuth authorization-code flow with PKCE and refresh tokens. Intended for ChatGPT/Claude remote connectors.

  • HTTP Basic: username MCP_CLIENT_ID, password MCP_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
/token

MCP_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.Content

Expected 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.Content

ChatGPT 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_chats

  • get_chat

  • list_unread_chats

  • list_messages

  • search_messages

  • get_message_context

Contacts and groups

  • search_contacts

  • get_contact

  • get_group

  • list_group_participants

Actions

  • send_text_message

  • send_group_message_with_mentions

  • reply_to_message

  • forward_message

  • react_to_message

  • send_saved_media

Media

  • get_media_metadata

  • list_saved_media

  • download_media

  • read_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_transcript

  • get_transcription_status

  • retry_voice_transcription

Metrics

  • count_inbound_messages

  • count_outbound_messages

  • count_active_chats

  • list_top_active_chats

  • list_unanswered_chats

  • get_median_first_response_time

  • get_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; download_media required

Video/GIF

Metadata only; download_media required

Sticker

Metadata only; download_media required

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=true

When 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, or disabled status

  • bounded 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-stream

Check application readiness:

$health = Invoke-RestMethod 'http://127.0.0.1:2785/api/health/ready'
$health

Read recent API logs or follow new entries:

docker compose logs --tail 200 api
docker compose logs -f api

Read transcription startup, model-download, checksum, and server logs:

docker compose --profile transcription logs --tail 200 whisper-model whisper
docker compose --profile transcription logs -f whisper

Stop 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 $headers

Stop or start the complete stack without deleting volumes:

docker compose stop
docker compose start

docker 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:

  1. 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_SECRET
  2. Start 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 ps
  3. Create and pair a secondary WhatsApp account using the QR or phone-number commands above. Require session status ready.

  4. Run MCP initialize and tools/list. Require HTTP 200 or 202 for initialize and HTTP 200 for tool discovery.

  5. Send one inbound text, document, voice note, image, and short video from another account.

  6. Confirm documents and audio save automatically. Confirm images and videos remain metadata-only until an explicit media-download tool call.

  7. When transcription is enabled, wait for voice transcript status success; verify text, detected language, model, duration, and linked message/media IDs. When disabled, require status disabled and no Whisper service.

  8. Review one read-only MCP result. Then test one write action against the secondary account after checking its exact chat and payload.

  9. 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 $headers
  10. Run 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 -- --runInBand

Compose 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_SECRET

MCP Inspector:

npx.cmd @modelcontextprotocol/inspector

Set 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 -d

Model 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 -d

VPS 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:

  1. Buy one Linux VPS per client.

  2. Clone this repository and create a unique .env.

  3. Bind the app to localhost only.

  4. Follow the runbook to create one named tunnel per client and route one HTTPS hostname to http://127.0.0.1:2785.

  5. Do not use Cloudflare's MCP portal.

  6. Apply Cloudflare WAF source-IP allowlists for the AI provider ranges you use.

  7. Keep OAuth enabled. WAF is defense in depth, not authentication.

  8. Set BASE_URL to the public HTTPS origin and exact provider callbacks in MCP_REDIRECT_URIS.

  9. 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_SECRET to revoke every issued access/refresh token.

  • MCP_REDIRECT_URIS uses exact matching. Never use wildcards.

  • /mcp requires 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

WhatsApp and Meta are trademarks of their respective owners. No affiliation or endorsement.

A
license - permissive license
-
quality - not tested
C
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

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

  • F
    license
    -
    quality
    C
    maintenance
    WhatsApp MCP server that exposes messaging, groups, contacts, and profile management as tools and resources for AI agents, supporting Baileys and Meta Cloud API.
    Last updated
    19
  • A
    license
    -
    quality
    A
    maintenance
    A 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 updated
    2
    MIT
  • A
    license
    -
    quality
    B
    maintenance
    A 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 updated
    1
    MIT

View all related MCP servers

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.

View all MCP Connectors

Latest Blog Posts

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