Skip to main content
Glama
LoopyOratory

OpenWA MCP Server

by LoopyOratory
README.md
# OpenWA MCP Server

Model Context Protocol (MCP) server that bridges AI agents to the [OpenWA](https://github.com/rmyndharis/OpenWA) WhatsApp API. Built with **Bun + Hono** and the official [`@hono/mcp`](https://github.com/honojs/middleware) transport.

Exposes **22 tools** covering messaging, chat/contact reads, and read-only session status — so an agent (MaxKB, Claude, Cursor, any MCP client) can read and send WhatsApp messages through your self-hosted OpenWA gateway. Session start/stop, webhook admin, group admin, and block/unblock are implemented in the OpenWA REST API but intentionally **not** registered as MCP tools (see [Tool allowlist](#tool-allowlist) below).

## Features

- **22 curated tools**, allowlisted from the full OpenWA REST API spec — no infra/admin surface exposed to the agent
- **Streamable HTTP transport** at `/mcp` (single port, no extra process)
- **Zod-validated** inputs on every tool
- **Optional bearer-token auth** on the MCP endpoint (`MCP_TOKEN`)
- **Zero config** — one env var (API key), works with the default OpenWA setup
- **Docker-ready** multi-stage build (oven/bun)

## Requirements

- Node 22+ (or just use the included Dockerfile)
- A running [OpenWA](https://github.com/rmyndharis/OpenWA) instance
- An OpenWA API key (`owa_k1_...`) — create one in the dashboard under **API Keys** (OPERATOR role for send tools)

## Quick Start

```bash
cp .env.example .env        # set OPENWA_URL + OPENWA_API_KEY
bun install
bun run dev                 # → http://localhost:3000/mcp
```

### Docker

```bash
cp .env.example .env
docker compose up -d --build
```

## Configuration

| Env var | Default | Description |
|---------|---------|-------------|
| `OPENWA_URL` | `http://localhost:2785` | Base URL of your OpenWA instance |
| `OPENWA_API_KEY` | — | OpenWA API key (**required**) |
| `MCP_TOKEN` | *(empty)* | Bearer token MCP clients must send. If set, requests without `Authorization: Bearer <token>` get `401`. Leave empty to disable auth (localhost only). |
| `PORT` | `3000` | Port the MCP server listens on |

## Connecting a client

Point any MCP client at the Streamable HTTP endpoint. If `MCP_TOKEN` is set, include it in headers:

```json
{
  "mcpServers": {
    "openwa": {
      "type": "http",
      "url": "http://localhost:3000/mcp",
      "headers": {
        "Authorization": "Bearer <MCP_TOKEN>"
      }
    }
  }
}
```

Verify the handshake (include the token if `MCP_TOKEN` is set):

```bash
curl -X POST http://localhost:3000/mcp \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -H "Authorization: Bearer <MCP_TOKEN>" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}'
```

## Tools (22)

**Sessions (read-only)** — `list_sessions` · `session_status`

**Send** — `send_text` · `send_menu` · `send_image` · `send_video` · `send_document` · `send_audio` · `send_location` · `send_contact` · `send_poll`

**Message actions** — `reply_message` · `forward_message` · `react_message` · `edit_message` · `delete_message`

**Read** — `chat_history` (live from WhatsApp) · `list_messages` (from local DB) · `list_chats`

**Contacts** — `contact_check` · `list_contacts` · `get_contact`

## Tool allowlist

The full OpenWA REST API surface is implemented in `src/mcp.ts`'s `openwa()` client, but only the tools listed above are registered on the MCP server — everything else (`session_qr`, `session_start`, `session_stop`, `send_sticker`, `block_contact`, `unblock_contact`, all `group_*` tools, and all `webhook_*` tools) is intentionally left **unregistered**, not just hidden. See the `ENABLED_TOOLS` allowlist and `tool()` wrapper near the top of `src/mcp.ts`.

Rationale: this server is meant to back a chat agent (see the Vivita-style system prompt pattern) that reads/sends messages and looks up chats/contacts — it has no legitimate reason to start/stop WhatsApp sessions, manage webhooks, or administer groups. Keeping those out of the registered tool set means a leaked `MCP_TOKEN` or a prompt-injected tool call can't touch infra state, even though the underlying OpenWA API key could technically do more. To re-enable a tool, add its name to `ENABLED_TOOLS` in `src/mcp.ts`.

## Project Structure

```
openwa-mcp/
├── src/
│   ├── index.ts      # Hono app + StreamableHTTP transport at /mcp
│   └── mcp.ts        # MCP server definition + tool allowlist + tools (Zod schemas)
├── Dockerfile        # multi-stage oven/bun build
├── docker-compose.yml
├── package.json
├── .env.example
└── README.md
```

## Notes

- **Two auth layers**: the optional `MCP_TOKEN` gates the MCP endpoint itself (clients send `Authorization: Bearer <token>`); the `OPENWA_API_KEY` is held server-side and forwarded as the `X-API-Key` header to OpenWA on every call. Clients never see the OpenWA key.
- **Send tools return `201`** = *accepted* by the gateway, not *delivered*. Check `contacts/check` first for new numbers.
- **`chat_history` requires the Baileys engine** on OpenWA (whatsapp-web.js does not support it — returns 501).
- **Media sends** accept either `url` or `base64` (+ `mimetype`); decoded size capped at 50 MiB by OpenWA.
- **No quick-reply buttons** — unofficial engines (whatsapp-web.js/Baileys) can't render interactive buttons; that's an official-Cloud-API-only feature. Use `send_menu` (numbered text menu) instead.
- **If you expose the server publicly**: set `MCP_TOKEN`, and use a dedicated session-scoped OpenWA key (OPERATOR at most) rather than a master key.

## License

MIT