Skip to main content
Version: v0.7.6

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.

Prerequisites

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

VariableDefaultEffect
MCP_READONLYunsetWhen true, mounts only read-tier tools.
MCP_RATE_LIMIT_MAX60Max tool calls per key per window.
MCP_RATE_LIMIT_WINDOW_MS60000Sliding 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.

DomainRead toolsWrite tools
Sessionlist, get, chats, statsmark read/unread, typing
Messagelist, history, reactionssend text/image/video/audio/document/location/contact/sticker/template, reply, forward, react
Contactlist, get, check-number, resolve-phone, profile-pictureblock, unblock
Grouplist, get, invite-codecreate, add participants, set subject, set description
Webhooklist, 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:

  1. Key extraction — the adapter reads the API key from the X-API-Key header or Authorization: Bearer ….
  2. Authentication — the call is validated by the same AuthService the REST guard uses, enforcing key validity, expiry, and the per-session allowedSessions scope before the handler runs. The tool's required role is enforced here too.
  3. Validation — the tool's input schema validates the arguments; a bad argument is rejected before reaching any service.
  4. 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" }
}
}
}
Keep .mcp.json out of version control

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

Do not give an MCP client an allowedIps key

A 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 allowedSessions scoping 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 (operator at most). This bounds the blast radius if the key leaks.
  • Read-only when you can. Set MCP_READONLY=true if 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.
Do not expose /mcp to the public internet

Authentication 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

SymptomCauseFix
Client can't connect to /mcpServer not enabledStart OpenWA with MCP_ENABLED=true and confirm it loaded on port 2785.
Tool list is empty or shows only readsMCP_READONLY=true is setUnset it to mount write tools, or keep it if observe-only is intended.
401-style auth failureMissing, expired, or wrong keyCheck the Authorization / X-API-Key header; mint a fresh key if expired.
Auth rejected for a valid keyKey carries an allowedIps listUse a key minted without allowedIps for MCP.
Write tool refused with a role errorKey role below operatorRe-mint the key with "role": "operator".
Tool acts on the wrong / no sessionKey not scoped to that sessionSet allowedSessions to include the target session id.
Calls start failing under loadPer-key rate limit hitSlow the agent, or raise MCP_RATE_LIMIT_MAX.

See Troubleshooting for general diagnostics.

Next steps