Expose OpenWA to AI agents over MCP
Turn OpenWA into a set of tools an AI agent can call. The Model Context Protocol (MCP) lets clients like Claude and Cursor invoke external capabilities as first-class tools. OpenWA ships an opt-in MCP server that exposes a curated slice of its functionality — listing sessions, sending messages, reading chats and contacts, basic group management — running through the same services and authentication as the REST API.
This guide shows you how to enable the server, connect a client, mint a safe key for it, and lock it down.
- A running OpenWA instance (v0.7.6). See Installation.
- At least one connected session to act on. See Connect your first session.
- An API key. See the Authentication guide.
- An MCP client (for example Claude Code or Cursor).
Why the server is off by default
The MCP server is disabled unless you opt in, and it is purely additive. When MCP_ENABLED is unset, none of the MCP code — and not the MCP SDK — is loaded, and every REST route behaves exactly as before. Turning it on mounts one extra route on the existing server; it does not start a second process or change any REST behavior.
The agent surface is also intentionally narrow. Rather than reflecting every REST route, OpenWA exposes a hand-picked, read/write-tiered set of tools. This keeps destructive and privileged operations off the agent's path and keeps the agent from drowning in choices.
Enable the server
Set MCP_ENABLED=true and start OpenWA as usual. The server mounts a stateless Streamable-HTTP transport at POST /mcp on the existing port (2785 by default).
MCP_ENABLED=true npm run start:prod # or set MCP_ENABLED in your .env / compose file
Note that /mcp sits at the server root, not behind the /api prefix used by REST endpoints. The full local URL is http://localhost:2785/mcp.
Optional tuning
| Variable | Default | Effect |
|---|---|---|
MCP_READONLY | unset | When true, mounts only read-tier tools. |
MCP_RATE_LIMIT_MAX | 60 | Max tool calls per key per window. |
MCP_RATE_LIMIT_WINDOW_MS | 60000 | Sliding window in milliseconds (default 1 minute). |
MCP_READONLY=true # mount read tools only — recommended when an agent only observes
MCP_RATE_LIMIT_MAX=60 # max tool calls per key per window
MCP_RATE_LIMIT_WINDOW_MS=60000 # sliding window in ms
The rate limiter is keyed by the authenticated API key id and is independent of the REST throttler. Any missing, blank, non-positive, or non-numeric value falls back to the default.
What the server exposes
The tool surface is an allowlist by construction: a capability is exposed only if a tool descriptor is written for it. There is no automatic reflection of REST routes. Each tool is tiered read or write, and write tools require an operator role.
| Domain | Read tools | Write tools |
|---|---|---|
| Session | list, get, chats, stats | mark read/unread, typing |
| Message | list, history, reactions | send text/image/video/audio/document/location/contact/sticker/template, reply, forward, react |
| Contact | list, get, check-number, resolve-phone, profile-picture | block, unblock |
| Group | list, get, invite-code | create, add participants, set subject, set description |
| Webhook | list, get | — |
Deliberately excluded (never exposed as tools): session lifecycle (create / delete / start / stop / force-kill), chat delete, bulk send, message delete, group leave / remove / promote / demote / invite-revoke, all API-key management, all plugin management, infrastructure import/export/restart, settings writes, and webhook create/update/delete. These are destructive, privileged, or have no agent use case.
How a tool call is authorized
The MCP server is a thin transport adapter over a protocol-neutral tool registry. Every tool call runs through the same pipeline as a REST request, in this order:
- Key extraction — the adapter reads the API key from the
X-API-Keyheader orAuthorization: Bearer …. - Authentication — the call is validated by the same
AuthServicethe REST guard uses, enforcing key validity, expiry, and the per-sessionallowedSessionsscope before the handler runs. The tool's required role is enforced here too. - Validation — the tool's input schema validates the arguments; a bad argument is rejected before reaching any service.
- Execution — the handler calls the existing service method and shapes its result through the same response DTO the matching REST controller uses, so no field is exposed over MCP that REST hides.
Because the transport is stateless, each request mints and tears down its own server instance — any request can hit any node, so MCP works behind a load balancer. See Scaling.
Connect a client
Point an MCP client at http://localhost:2785/mcp and supply your API key in a header. For a project-scoped client such as Claude Code, drop a .mcp.json at your project root:
{
"mcpServers": {
"openwa": {
"type": "http",
"url": "http://localhost:2785/mcp",
"headers": { "Authorization": "Bearer YOUR_API_KEY" }
}
}
}
.mcp.json out of version controlIt carries a live API key. Add it to .gitignore. Replace YOUR_API_KEY with a real key — mint a dedicated one as shown below.
From the client, list the tools. You should see the curated set above, or only the read tools under MCP_READONLY. Then call one — for example SessionFindAll — to confirm authentication and execution end to end.
Mint a dedicated key for the agent
Do not reuse an admin key. Create a least-privilege, session-scoped operator key for the MCP client. Create it once over REST; the plaintext key is returned only in this 201 Created response under apiKey, so copy it immediately.
curl -X POST http://localhost:2785/api/auth/api-keys \
-H "X-API-Key: YOUR_ADMIN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "claude-mcp",
"role": "operator",
"allowedSessions": ["my-session-id"]
}'
The call returns 201 Created with the one-time plaintext key in apiKey:
{
"id": "a1b2c3d4-0000-0000-0000-000000000000",
"name": "claude-mcp",
"keyPrefix": "owa_k1_a1b2",
"role": "operator",
"allowedSessions": ["my-session-id"],
"isActive": true,
"usageCount": 0,
"createdAt": "2026-06-25T09:30:00.000Z",
"apiKey": "owa_k1_0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
}
Paste the apiKey value into your .mcp.json — it is returned only in this response. To rotate, create a new key and delete the old one — there is no way to retrieve an existing key's plaintext later. See the full request and response shapes in the API reference and the Authentication guide.
allowedIps keyA tool call carries no genuine client IP, so a key that lists allowedIps is rejected over MCP. Mint the MCP key without an allowedIps restriction. Apply IP restrictions to other keys instead.
Security
The MCP server reuses REST's security model rather than re-implementing it, but the agent context changes how you scope keys.
- Same authorization as REST. Role and per-session
allowedSessionsscoping are enforced identically to REST. A key scoped to one session cannot act on another. - Least privilege. Give each MCP client a dedicated, non-admin, session-scoped key (
operatorat most). This bounds the blast radius if the key leaks. - Read-only when you can. Set
MCP_READONLY=trueif the agent only needs to observe. Write tools are then never mounted. - Response parity. Tools reuse the REST response DTOs, so fields the REST API strips — webhook secrets and custom headers, session proxy URLs, engine config — are not exposed over MCP either.
- Rate limiting. The per-key limiter bounds how fast a single key can call tools, independently of the REST throttler. Tune it with the env vars above.
/mcp to the public internetAuthentication today is a static API key, which is appropriate for a self-hosted instance reached locally or over a trusted network. Do not expose /mcp publicly without a fronting authentication proxy. OAuth 2.1 support for public, internet-facing deployments is planned but not yet available in v0.7.6.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
Client can't connect to /mcp | Server not enabled | Start OpenWA with MCP_ENABLED=true and confirm it loaded on port 2785. |
| Tool list is empty or shows only reads | MCP_READONLY=true is set | Unset it to mount write tools, or keep it if observe-only is intended. |
401-style auth failure | Missing, expired, or wrong key | Check the Authorization / X-API-Key header; mint a fresh key if expired. |
| Auth rejected for a valid key | Key carries an allowedIps list | Use a key minted without allowedIps for MCP. |
| Write tool refused with a role error | Key role below operator | Re-mint the key with "role": "operator". |
| Tool acts on the wrong / no session | Key not scoped to that session | Set allowedSessions to include the target session id. |
| Calls start failing under load | Per-key rate limit hit | Slow the agent, or raise MCP_RATE_LIMIT_MAX. |
See Troubleshooting for general diagnostics.
Next steps
- Send messages — the workflow most agent tools drive.
- Authentication — roles, scoping, and key rotation in depth.
- Scaling — run MCP behind a load balancer.
- API reference — the REST endpoints each tool mirrors.