Skip to main content
Glama
razvangirgiz

wazap-mcp

by razvangirgiz
██╗    ██╗ █████╗ ███████╗ █████╗ ██████╗
██║    ██║██╔══██╗╚══███╔╝██╔══██╗██╔══██╗
██║ █╗ ██║███████║  ███╔╝ ███████║██████╔╝
██║███╗██║██╔══██║ ███╔╝  ██╔══██║██╔═══╝
╚███╔███╔╝██║  ██║███████╗██║  ██║██║
 ╚══╝╚══╝ ╚═╝  ╚═╝╚══════╝╚═╝  ╚═╝╚═╝

WhatsApp for your AI agent. An MCP server that puts your WhatsApp account — chats, messages, media, contacts, groups — behind tools any MCP client can call. Pairing-code login, no browser, no phone-number reseller, ~20 MB of RAM.

Built on Baileys, which speaks the WhatsApp multi-device protocol over a WebSocket.

Get started

The npm package is wazap-mcp; the command it installs is wazap.

npx wazap-mcp setup

That is the whole install. It links your account, finds the MCP clients installed on this machine, writes their config, copies the five skills where that client reads them, and tells you what to restart. At a terminal it is one black, centered screen per step: ghosted ASCII logo, step number, then the QR or the question. Piped output stays a log. When you started through npx, setup offers to install wazap globally so Claude Desktop and the background service have a path that does not change. It also offers to brew install whisper-cpp, ffmpeg or Tailscale when a step needs one and it is missing, and to restart Claude Desktop itself once it has connected it.

Or the path your harness prefers

Harness

Fastest path

Claude Code

/plugin marketplace add razvangirgiz/wazap, then /plugin install wazap@wazap

Claude Desktop

download wazap-<version>.mcpb from Releases and double-click it

Gemini CLI

npx wazap-mcp connect gemini

Cursor

the Install in Cursor badge, then npx wazap-mcp skills install cursor

Codex CLI

npx wazap-mcp connect codex, then npx wazap-mcp skills install codex

A hosted agent (claude.ai, ChatGPT)

a URL it signs in to: Keep it running

Anything else

the MCP entry npx -y wazap-mcp over stdio

Each local harness registers the server; a hosted agent gets a URL. Linking the WhatsApp account is a separate, one-time step: npx wazap-mcp login.

Or have your agent do it. Paste this:

Set up WhatsApp for me: run npx wazap-mcp setup --agent and follow what it prints.

Then ask your agent: "what did I miss on WhatsApp today?"

Below are the steps setup runs for you. Each is still its own command when you want to run it by hand.

npx wazap-mcp login shows a QR code; scan it from Settings → Linked devices → Link a device. No camera handy, or linking over SSH? npx wazap-mcp login --phone +15550100 prints an 8-character code you type under Link with phone number instead. It ends by asking whether the agent may send messages; the answer is no unless you say yes, and npx wazap-mcp config writes on changes it later.

npx wazap-mcp connect claude-code writes the MCP entry for one client. The table under Connect a client has the rest.

npx wazap-mcp on its own is safe to run: it prints where you stand and what to do next, and starts no server. When something is off, npx wazap-mcp status is the first thing to run — it checks Node, the data directory, the lock, the credentials and whether a newer version is out, and prints the fix next to anything broken.

Connect a client

wazap connect <client> writes the entry for you, keeping whatever else is in the file and backing it up once before the first change. --dry-run shows what it would write.

Client

What connect writes

claude-code

runs claude mcp add whatsapp for you

claude-desktop

claude_desktop_config.json in the Claude application directory

cursor

~/.cursor/mcp.json

codex

[mcp_servers.whatsapp] in ~/.codex/config.toml

gemini

~/.gemini/settings.json

anything remote

client's MCP URL field: https://your-host/mcp, signed in with OAuth (see Keep it running)

Other MCP clients

Cursor installs from a link:

Install in Cursor

It carries the same entry connect writes; node scripts/badges.mjs reprints it.

Any other MCP client works the same way: the command is npx -y wazap-mcp, the transport is stdio. Tell the agent to call learn first — it returns the id formats, the workflows and every error code with what to do about it.

{
  "mcpServers": {
    "whatsapp": {
      "command": "npx",
      "args": ["-y", "wazap-mcp"]
    }
  }
}

Claude Desktop, Cursor and Gemini CLI take exactly that. Codex CLI is TOML:

[mcp_servers.whatsapp]
command = "npx"
args = ["-y", "wazap-mcp"]

The skills/ folder follows the Agent Skills format, so Codex, Cursor and other skill-aware agents can load the same five skills.

Claude Desktop, without a terminal

Download wazap-<version>.mcpb from Releases and double-click it. Claude Desktop installs the server, its Node dependencies and the icon, and shows two settings: Read-only, ticked, and Data directory, empty. wazap connect claude-desktop does the same job by editing claude_desktop_config.json. Claude Desktop starts its servers without your shell PATH, so that entry is the absolute path to node when wazap is installed globally, and npx otherwise; wazap setup checks that the entry it wrote is one Claude Desktop can actually launch.

Then ask Claude to link your WhatsApp. It calls link_account with your number, hands back an 8-character code, and you type that code into WhatsApp → Settings → Linked devices → Link a device → Link with phone number instead. No terminal at any point. npx wazap-mcp login does the same job from a shell when you have one.

Untick Read-only to let Claude send. It ships ticked because a bundle that can message people from your number before you have said so is the wrong default, and because the setting cannot be left unanswered: the manifest format has no way to omit an argument, so the box you see is the answer the server gets. link_account is registered either way. Read-only exists to stop Claude messaging people from your number, and relinking your own dead session messages nobody.

Build it yourself with npm run bundle:mcpb, which stages dist/, the manifest, the icon and a fresh production node_modules, then packs them with @anthropic-ai/mcpb.

Keep it running

A wazap started by a client lives as long as that client does. Quit Claude Code and the session is gone until you open it again. Two commands change that. Staying up and being reachable are separate choices.

npx wazap-mcp service install

That writes a launchd agent on macOS (~/Library/LaunchAgents/com.wazap.server.plist) or a systemd user unit on Linux (~/.config/systemd/user/wazap.service), starts it, and waits for /healthz to answer. The unit runs serve --http on 127.0.0.1:8766 with the absolute path of this Node and this install, so it survives a reboot and a logout. Point any client at http://127.0.0.1:8766/mcp, or keep using the stdio entry. A second wazap on the same data directory becomes a bridge onto the session this one holds.

service status prints the pid, the health check and whether the unit still runs the version you have installed. service logs tails it. service restart picks up an upgrade; service uninstall removes the unit and leaves your session and credentials alone. wazap login needs the session to itself, so it stops the service, pairs, and starts it again on its own. wazap logout and wazap account changes do not stop anything: the running server applies them.

A sleeping Mac is an offline wazap. System Settings → Lock Screen, or Battery → Options, has the switch that keeps it awake on power.

npx wazap-mcp expose

That gives the running service a public https URL, for agents that are not on this machine: a cloud agent, claude.ai, ChatGPT. It uses Tailscale Funnel if tailscale is installed, Cloudflare Tunnel if cloudflared is, opens the tunnel, writes WAZAP_PUBLIC_URL and a fresh WAZAP_OAUTH_PASSWORD into <data-dir>/.env, restarts the service and checks the URL from here. It then prints the MCP URL and the password once.

Give an agent the URL only. It signs in on a consent page on your own host with that password and picks read or read-and-send there; wazap status lists who holds a grant. See Hosted agents (OAuth) for what that page does. npx wazap-mcp expose off takes the tunnel down and keeps the password, so the next expose hands agents the same one.

npx wazap-mcp setup asks all of this once, as its fourth step.

Upgrade

npx wazap-mcp update

One command for what used to be three. It compares this install against the registry, installs the new package when wazap is global, restarts the service so it runs the new code, and copies the new skills into every harness that keeps them. --dry-run prints the plan and touches nothing.

Related MCP server: wa-bridge

Tools

Tool

Kind

What it does

learn

read

The guide to every tool, id format and error code, as text. Call it first.

get_status

read

Connection status, sync state, linked account, how fresh the history is, webhook delivery, versions, data dir. Top-level fields are the default account; accounts lists every configured one and default names it. Optional account_id on this and every other tool.

link_account

read

Pair an account that already exists (wazap account add). Returns the code to type into the phone. Registered in read-only mode too.

list_chats

read

Conversations newest-first; filter all/unread/groups/individual/archived.

read_messages

read

Messages in a chat; before pages further back, pulling older history from the phone; types narrows to one or more message types, e.g. ["call"]; include_previews attaches a small image of each photo. chat_id: "status" reads the stories of the last hours, which show nowhere else.

catch_up

read

What the user missed, in one call and within a token budget, across every linked account: who is waiting on a reply, mentions, replies and open polls, missed calls, people, groups condensed, stories. Pages with a cursor. See Catching up.

search

read

Messages by meaning and by words at once, over everything the account keeps, so a paraphrase or another language still hits; match: "words" keeps only messages holding the words. chat_id, since, until and from narrow it, and the answer says how much it searched. Without chat_id, someone tagged #private is left out and counted in private_omitted. Without semantic recall it matches words and says so.

get_message

read

One message in full, with its quoted message, each reaction with who left it, and who voted for each option of a poll or answered an event. On your own messages, delivery says whether it was sent, delivered, read or played, and in a group who read it and when.

find_contact

read

Who a name, nickname, relationship ("mama"), group name, number or id means. Resolved: the chat_id, number, note, tags and details, plus the recent exchange and how you write there in a session that can send. Otherwise the candidates that tell people apart, to ask you. tag lists everyone filed under a tag. See Finding people.

get_group_info

read

Participants, admins, announcement mode, who may edit the info or add members, join approval, disappearing messages, community, invite link (when you are admin).

get_media

read

A message's media: a voice note or audio as its transcript, a photo attached as an image, any file saved to disk (save_to picks the directory). Transcription runs on the local or the API provider; with save_to a recording comes as its file, with a transcript only if one was already made, and when no transcript can be made the file comes instead, with transcript_unavailable saying why.

wait_for_messages

read

Block up to 55 s until a message arrives, then return it with a cursor for the next call. addressed_to_me wakes only for direct messages, @-mentions and replies.

remember

local

Keep what the user says about a person, on this machine only: a note, tags, details (relatie, nickname, "role": "contabil") that find_contact matches, and handled: true to take an ask off catch_up's waiting list until they write again. #private keeps their words out of what the assistant did not ask about them by name (Keeping someone private). Nothing changes on WhatsApp.

send_message

write

Draft a message: text (a reply, @-mentions), media from a path or URL (as: document, voice note or GIF), a poll (options), a location (latitude, longitude) or a forward (forward). Does not send. A text draft to someone you write to often carries style_check: where it does not read like you.

confirm_send

write

Send a draft after the user has seen the preview and said yes. A draft is sent at most once; see Sending once.

edit_message

write

Edit your own message, within WhatsApp's 15-minute window.

react_to_message

write

Add or remove an emoji reaction.

delete_message

write

for_everyone: true retracts your own message, within WhatsApp's 2-day window, and in a group where you are admin someone else's message too. for_everyone: false deletes any message for the linked account only, at any age.

manage_chat

write

Archive, pin, mute (8h by default), mark read/unread; pin a message for everyone (24h, 7 days or 30 days) or star it; clear or delete the chat for the linked account; block or unblock a person.

manage_group

write

Create a group; join one from an invite link or message (without confirm: true it only shows the group); add, remove, promote, demote, leave, rename, set or remove the group photo, invite links, list, approve or reject join requests, and change the settings. Every member sees a change at once.

Sending once

Nothing reaches WhatsApp until confirm_send, and a draft goes out at most once, even across a crash. Drafts are kept in the account database for 15 minutes, at most 20 per MCP session and 200 per account, each with the WhatsApp message id it will be sent under. Confirming a draft again answers the same receipt with already_sent: true, and two confirms at once send it once. A failure while the message is still being prepared (not connected, the write budget, the number lookup, a missing file, a media upload) leaves the draft as it was, to confirm again. Once the message is handed to WhatsApp's relay (which also looks up the recipient's devices and encrypts it before writing), a failure answers SEND_OUTCOME_UNKNOWN: WhatsApp may have the message, so that draft is never sent again. The agent checks the chat instead. When WhatsApp later echoes that message id, the send is recorded as sent, and the session that confirmed it gets the receipt from then on. For 24 hours a confirmed draft answers its receipt or SEND_OUTCOME_UNKNOWN to that session; deleting the sent message, or clearing or deleting its chat, removes its words from the record. MCP sessions do not survive a restart: after one, no session can confirm a draft made before it, sent or not, though a send the restart interrupted is still recorded as unknown and still settles when its id is echoed.

Catching up

catch_up answers "what did I miss?" in one call. It reads the account database only — no network, except the cached member list of at most a dozen groups, fetched within a second — and fits its answer into budget_tokens (2,500 by default, 500 to 8,000), one line per entry, in this order:

  1. Waiting on you: people whose last word asks for something — a question mark, a request word, or a voice note nobody has heard, never "ok, thanks" or a link — with the ask quoted (a voice note by its transcript) and what they said after it. In a group only when the user was @-mentioned or replied to. An ask stays until the user answers, remember marks it handled or it is two weeks old; new marks one that arrived since the last catch-up, and an answered call after the ask says it may have been dealt with by phone.

  2. Mentions, replies and polls: group messages that @-mention the user or reply to them, and polls and events they have not answered, muted and archived groups included.

  3. Missed calls, one line per person, saying whether the user called back or wrote since.

  4. People who wrote: saved contacts first, then by how much they wrote; business accounts, numbers nobody saved and muted chats last. One quote each, with the media counted by kind.

  5. Groups, one line each: how many messages, from how many people, the three who wrote most, media, polls, and a quote when the budget allows. Muted and archived groups share one line.

  6. Stories: how many, and from whom.

A footer names the voice notes nobody transcribed (for get_media) — only counting those of someone tagged #private — and counts what was left out: chats tagged #no-catchup, groups the user left, channels and broadcast lists. Signals — an amount, a date, a time, an address, a link, a question — are marked on the entries shown, and an ask carrying a sum or a date moves up. A chat's messages count as missed only after the user's own last message there and after what their phone already read.

The mark. Each client keeps its own mark per account: the OAuth client, the token (WAZAP_READ_TOKEN and WAZAP_WRITE_TOKEN are two clients), or for stdio and the clients sharing a running wazap the MCP client's own name (local:claude-code, local:cursor), so two assistants on one machine do not share a mark. By default a catch-up reads since that client's last complete one, and moves the mark once all of it was given: a digest with no more, or the last page of one. The mark follows what reached wazap, not the time a message carries, so a message filed late — a missed call stored when it stops ringing, a message decrypted on a retry, one from a phone whose clock runs ahead — is in the next catch-up rather than under the mark; nothing sent more than two weeks ago counts. The first time, or when the mark is more than a week old, it reads the last 24 hours and says so. since: "previous" gives the last catch-up again; hours: N (up to 336) or since as an ISO date or time (2026-09-16, 2026-09-16T18:00, an offset optional) from the last 14 days read an explicit window and leave the mark where it is, and so does a catch-up limited by include. No window reaches further back than 14 days. Two catch-ups of one client at once move the mark once. The mark moves when the last page is answered, before the answer is on its way, so a catch-up is given at most once: if the answer is lost (a dropped connection, a client that crashed), since: "previous" gives it again. Nothing is marked read on WhatsApp.

Paging. When the entries do not fit, the answer ends with more: how many are left per section, about how many tokens they take, and a cursor. The first page works out the whole digest and holds it, so the next pages give exactly the rest of it, whatever arrives or is read on the phone in between. A cursor is a random id that only the client that got it can use, and it lasts 15 minutes past its page; after that, or after a restart, it is CURSOR_EXPIRED: call catch_up again without it, the mark has not moved.

Several accounts. Without account_id, a catch-up covers every linked account at once, each section labelled per account, sharing the budget. A disconnected account is reported as disconnected, with what it had stored, and keeps its mark.

Leaving a chat out. Tag a person #no-catchup with remember (an agent, a bot, a busy notification number) and catch-ups skip their chat, counting it in the footer, and nothing they send elsewhere shows either: no ask, mention, poll or quote of theirs in a group, no group call, no story. Tag them #private instead and they stay in, counted, but nothing they wrote is quoted — not the ask, not a mention or a poll of theirs in a group, not a group's quote — and their entries say private (Keeping someone private). A person tagged on any linked account is tagged on every account a catch-up reads, by number or lid, with account_id or without.

Seeing, waiting, following up

include_previews: true on read_messages attaches a small JPEG of each photo as an image block, newest first, up to 12 per call, and labels each message line with the preview it belongs to, so a catch-up can say "a photo of a receipt" without a download. WhatsApp used to ship such a preview inside every image message and in 2026 almost never does, so when none is there wazap downloads the photo once, shrinks it to 320 px on this machine with pure JavaScript, and keeps the result as a file under previews/ in the data directory, so a restart does not redo it. A video gets one frame, taken by ffmpeg a second in, when ffmpeg is installed. The first call over a day of photos takes a few seconds; the next is instant.

wait_for_messages blocks until something arrives, up to 55 seconds, then returns it with a cursor. Calling it again with that cursor replays whatever landed in between, so an agent can sit in a loop and miss nothing. With addressed_to_me only direct messages, @-mentions of the user and replies to their messages wake it; group chatter does not. The user's own messages and WhatsApp's notices never do.

Every message comes back with a non-empty text: media and system messages carry a placeholder such as [image] caption, [voice message · 0:42], [deleted] or [poll] Pizza or pasta?. A poll also carries each option with who voted for it, and an event who answered going, maybe or not going. Timestamps are ISO 8601 with the machine's UTC offset, alongside a human age like 2h ago.

Finding people

find_contact answers "who is mama?", "Ana de la contabilitate" or "Mișu" before anything is drafted. It reads the names wazap keeps for a person — the saved contact name, a business name, the name they give themselves — and what you filed about them: a nickname or relatie detail, a tag, and a note that says nothing but the relationship ("mama"). Case, diacritics and Romanian case endings do not matter ("Stefan" is Ștefan, "mamei" is mama), a short form finds the full name ("Mișu" is Mihai) below the name itself, and a group is found by its name. A relationship word matches only what you filed, never a message and never a name like "Mama Anei" or "Mamaia Resort". People you talk to more, and more recently, rank higher. qualifier tells two of a name apart: a tag, a detail, a note, a business or a group they write in ("contabilitate"), or the last four digits of the number.

The answer is one of three:

  • resolved — one person or group is clearly meant: contact.chat_id, the full id to send to, with what matched.

  • ambiguous — up to five candidates and what tells them apart: when you last exchanged messages and in which direction, how many you sent them in 90 days, groups in common, your note and tags, whether it is a business, and the number's last four digits. No candidate carries a full number or a word of any message, so the agent has to ask you and look the one you name up again.

  • not_found — the closest names, if any. For a relationship nobody is filed under, the agent is told to ask who it is and file it with remember (fields: {"relatie": "mama"}).

Without account_id, every linked account is searched and each candidate says which account it is on; the answer is resolved only when one account has the only match. A resolved person also comes with their number and what the user filed on them (note, tags, details). A number, however it is written ("+40 722 001 111", "0722-001-111"), or an id (a sender's id from a message) is looked up as such: resolved with matched.source number or id, or not_found telling the agent to check the number. find_contact({ tag: "client" }) lists everyone filed under a tag instead, each with their chat_id: up to limit (50), shared between the accounts, with omitted counting on each account whoever the limit left out.

Draft context. A resolved contact also carries what a message to them is written after: the last 8 messages both ways (each cut to 200 characters, voice notes as their transcript) and how you write there — language, diacritics, tu or dumneavoastră, length, emoji — from your own messages in that chat in the last 90 days, or across the account when there are fewer than five. The style never counts messages wazap sent, so an agent does not learn its own drafts back. It is on by default, only in a session that can send, and only for a resolved contact the account's send rules allow. A contact tagged #private (remember with add_tags: ["private"]) on any linked account gets the style only, never messages, and a group's context leaves out what they wrote (Keeping someone private). wazap config draft-context off [--account <id>] turns it off for an account (draft_context: false in accounts.json), the style check below included, from the next call, without a restart.

Style check. A text draft to a person you have written to yourself (not through wazap) at least five times in 90 days comes back with style_check: warnings among language_mismatch, diacritics_mismatch (with diacritics where you write without them, or words like „mâine” without them where you use them), address_mismatch (tu where you say dumneavoastră, or the reverse) and length_outlier (over three times your usual longest there, and over 80 characters), with the basis it was measured on. What the draft quotes does not count, nor "doamna" said of someone else, nor a plural spoken to two people. It never blocks a draft; words you dictated stay as they are.

The address book. Names come from the phone. When no contact has a saved name yet, the first find_contact of a server run asks WhatsApp for the address book, the way wazap contacts resync does, and waits up to 15 seconds for it before answering; finds that arrive meanwhile wait for the same answer. It does not ask while the connection is still receiving its first sync, nor again within 7 days of the last ask, the same rule wazap heals a missing address book by at connect.

Keeping someone private

Tag a person #private (remember with add_tags: ["private"]) and their words — a message's text, caption, transcript, quote, a link's or a file's preview, a poll's text — stay out of what the assistant did not ask about them by name. A call names them when it gives their chat (chat_id), a message of theirs (message_id, to get_message or get_media), or them as the author (search with from); a group named by chat_id reads whole, what they wrote in it included. Everywhere else what is theirs keeps who, when, in which chat and what kind, and loses the words:

  • catch_up counts them and never quotes them; their entries say private.

  • find_contact's draft context carries your style for them, no messages.

  • search without chat_id leaves out their chat and what they write in groups before it counts to limit, by meaning and by words, and says how many in private_omitted. A quote of theirs in someone else's message keeps who wrote it, not what.

  • wait_for_messages without chat_id still returns what arrived from them, in their chat or in a group, with text: "[private]" and private: true; waiting on one chat, theirs or a group's, reads it whole.

  • list_chats shows the last message of their chat, or the last one they wrote in a group, the same way.

  • read_messages on "status" keeps their stories with author, time and kind, without text, caption or preview. A story cannot be named, so for now there is no way to read one of theirs through the assistant.

The tag goes with the person: filed on one account, it holds in search, wait_for_messages, list_chats, the stories and find_contact's draft context of every other account, by number or lid, and in every catch-up. The outbound webhook is not affected; it is the channel for what you build, not the assistant's.

Voice messages

A voice note is the one message an agent cannot read. Switch transcription on and it becomes text: [voice message · 0:42] "sunt la notar, ajung în 20 de minute", with the bare words also in a transcript field. catch_up and search see that text, so a voice note becomes findable by what was said in it.

Pick a provider once, in wazap setup or later:

wazap config transcribe local     # free and private, one 574 MB model on disk
wazap config transcribe openai    # cheap and fast, the audio leaves this machine
wazap config transcribe off

local

openai

Runs

whisper.cpp, here

any OpenAI-compatible /audio/transcriptions

Costs

nothing

per minute of audio, on your key

Privacy

the audio never leaves this machine

the audio leaves this machine

Needs

whisper-cpp and ffmpeg, plus a model

an API key

Local, with whisper.cpp

brew install whisper-cpp ffmpeg      # macOS; elsewhere build whisper.cpp, install ffmpeg from your package manager
wazap transcribe download            # fetch and verify the model
wazap transcribe test recording.ogg  # prove it before you trust it

wazap setup and wazap transcribe download offer that brew install themselves when either binary is missing, and go straight on to the model in the same run. --no-brew turns the offer off everywhere.

Models land in <data-dir>/models/ and are checked against a SHA-256 pinned in the source. The shared whisper/embedding downloader stops an oversized response before excess bytes are written, independently of Content-Length. Only a successfully closed write with the exact size and digest is renamed from .part to the final model file.

The network/write phase has a 30-second no-progress timeout (including waiting for response headers) and an overall deadline of 30 minutes or the time the model takes at 100 KiB/s, whichever is longer (about three hours for large-v3). A timeout or interrupted transfer keeps a bounded partial file for a later retry to resume; an invalid range, oversized response or failed verification discards it. A receiver that ignores Range restarts the download safely. CDN redirects remain supported, but compressed responses are refused so byte ranges remain unambiguous. Errors report status/category, not signed URLs, response excerpts or raw disk errors.

Each destination has an exclusive <model>.download-lock/ directory, held from cache verification through the final rename and cleanup. A simultaneous download of that model fails promptly with a retry hint; different models can download in parallel. Directory symlinks and relative paths use the same canonical parent. The lock is released on success, handled failures and cancellation; if it cannot be removed, the verified model is kept and the next run names the directory.

A known dead owner on the same host/PID scope can be recovered automatically; Linux also checks the PID namespace. Live owners are never evicted by age. If a process dies during lock initialization/cleanup, or the owner record is corrupt, from another scope or inaccessible, recovery fails closed. Inspect the owner-*.json inside the lock directory and remove only that lock directory only after confirming no downloader is still using the model. Then rerun the command to reuse the partial file when possible. Never remove an active lock. This coordinates cooperating versions on one host, not distributed downloads across machines; stop older downloaders before upgrading.

The model is whisper large-v3-turbo (ggml-large-v3-turbo-q5_0.bin, 574 MB), the smallest that still gets Romanian right. Smaller models drop diacritics and mangle names, which is worse than no transcript at all: a missing transcript is a question, a wrong name is a wrong answer.

An API, OpenAI-compatible

wazap config transcribe openai asks for the key without echoing it, then for the base URL (OpenAI unless you type another), and stores both in <data-dir>/.env.

With this provider the audio leaves your machine. Every voice note wazap transcribes is uploaded to that endpoint. If that is not acceptable, use local, which uploads nothing.

The key is treated as a secret rather than as a setting:

  • It is never accepted as a command-line argument, because an argument lands in your shell history and in ps.

  • The prompt echoes nothing, not even asterisks.

  • It is stored only in <data-dir>/.env, mode 0600.

  • status, status --json, config and get_status show at most api key: set (…abcd).

  • Provider error bodies, transport exception details and malformed-JSON excerpts are not printed. Errors retain HTTP status, timeouts and actionable fixes.

  • A plain-http base URL is refused unless it points back at this machine. Userinfo credentials, queries and fragments are not allowed in this base URL; set the API key separately.

  • Redirects are refused, including same-origin redirects: configure the final base endpoint directly. This keeps audio and credentials on the intended route.

  • Successful JSON responses are capped at 1 MiB, including chunked responses. Error response bodies are discarded without being read.

Without being asked

With a provider configured, incoming voice notes of up to ten minutes are transcribed in the background as they arrive, never holding up a message. The transcript is stored with the message, so a voice note is transcribed once, and its words are searchable, recalled and carried by the webhook event.

  • Durable. The note is queued in the account database in the same transaction that stores it, so a restart or a crash resumes the queue instead of dropping it. A note that just arrived starts at once, ahead of any backlog, which is what lets its webhook event carry the words. A stop waits up to 30 s for a transcription under way to store its words, so a note is not paid for twice; removing an account cancels it instead.

  • One at a time for the whole server. Every account shares one transcriber and they take turns, so a backlog on one does not starve another and two whisper.cpp runs never fight for the machine. Only the server (wazap serve, the service) transcribes; short commands such as wazap status --live queue what arrives and leave it to the server.

  • Retried, then given up on. A download that times out, a provider answering 429 or 5xx, or whisper.cpp crashing is tried again after 10 s and after a minute more, three attempts in all. Media WhatsApp no longer holds, audio the provider refuses as input, or a file too large gives up at once. A note given up on is not queued again; get_media(message_id) still tries it on request.

  • Waiting costs nothing. A note whose account is disconnected spends no attempt and runs within seconds of the connection opening. A provider that cannot take any note — whisper.cpp or its model missing, an API refusing the key — pauses all transcription for 30 s, then twice as long each time up to 15 minutes, and one note probes it before any other audio is downloaded. Meanwhile webhook events post the [voice message · 0:42] placeholder at once instead of waiting for words that are not coming.

  • Deleted means dropped. A note deleted, expired or cleared while it waits leaves the queue and is never uploaded.

  • A day at most. A note still waiting 24 hours after it was queued (the account offline, the provider paused) is given up on as too_old and never transcribed on its own later.

  • Local stays local. Each note remembers whether it was queued for local or for an API. A note queued under local is never sent to an API provider configured afterwards: it is given up on as provider_changed. A note queued for an API may still be transcribed locally.

  • History: the last day only. A note that a history sync brings (a first link, a relink) is queued only when it is less than 24 hours old, so linking never transcribes the archive. A note WhatsApp delivers live is always queued, however old its timestamp.

Audio files are left alone, since one can be an hour long, and so are notes you recorded and notes WhatsApp gave no length for; call get_media(message_id) for those. WAZAP_TRANSCRIBE_AUTO=0 keeps that and stops the background work; with it, or with the provider switched off, a queue already stored is kept and waits, and it continues under the provider configured next, within the day and the local-stays-local rule. get_status shows the queue under transcription (how many wait, how long the current run has taken, how many were given up on, the latest reason, a pause and until when, never content), and wazap status prints a voice queue line, a warning when notes wait and nothing will run them.

Semantic recall

With recall on, search matches what was meant and the words at once: a paraphrase or another language still hits through its meaning, a short or foreign-language question through its words, and the two rankings are fused. It reaches every message the account keeps. For an exact string — an id, a phone number, a URL — pass match: "words". When meaning search cannot run — recall off, the embedding server failing or refusing the query, or the sidecar still starting after 8 s — search matches the words only and says so (mode: "keyword_fallback", with recall_unavailable naming the cause and, for recall off, the command that turns it on).

Off by default, and fully local: a llama-server sidecar bound to loopback does the embedding, so nothing leaves the machine. It needs llama.cpp, the pinned model and persisted history (WAZAP_PERSIST_HISTORY, on by default):

brew install llama.cpp      # macOS; elsewhere build llama.cpp and put llama-server on PATH
wazap embed download        # fetch the embedding model, ~318 MB sha256-verified
wazap config recall local   # then restart the service

wazap embed download offers the brew install itself when llama-server is missing. wazap status runs the three checks — recall, llama-server, embed model — and get_status reports the index as off, indexing, ready or degraded.

chat_id, since, until and from narrow a search by meaning exactly as they narrow one by words. Hits rank by a fused score (reciprocal rank fusion of the word and meaning rankings), and a hit found only by meaning must clear the similarity floor, so a question with no answer comes back empty. A match found by meaning weighs a little less with age — 85% a month on, never under 70%, for its rank and for the floor — so the fresher of two close matches comes first while a clearly closer old one still does, and a word hit whose meaning falls under the floor ranks by its words alone. One chat takes at most three leading places before other chats' hits, and a near-duplicate trails the list. The vectors live in the account database next to their messages, are made in the background for every message that has none, and leave with their message when it is deleted, revoked or expires; an edit makes its vector again. A message wazap holds only as text — carried over from the recall index an older wazap built — is marked from_index: get_message returns its text, but get_media has nothing to open and it cannot be replied to or forwarded.

Embedding requests refuse redirects, cap replies at 4 MiB and validate vector shape and finite values. Provider bodies and decoder stderr are not copied into errors.

wazap config recall local|off sets WAZAP_RECALL, the one setting recall has. The model is embeddinggemma-300m, and every kept message is indexed.

Skills

wazap ships five Agent Skills that teach an agent the workflows behind the tools, not just the tools:

Skill

What the agent does

wazap-setup

Diagnose with wazap status, link by QR or pairing code, connect a client with wazap connect, repair an expired session

whatsapp-inbox

"What did I miss?" Triage into needs you / FYI / noise, ranked, plus forgotten replies. Read-only

whatsapp-recall

"Find the invoice Dan sent." Search with query variants, page back in time, download and read the file. Read-only

whatsapp-groups

Catch up on a 300-message group: decisions, dates, what is asked of you. Read-only

whatsapp-send

Find who the user means, draft in the chat's own register, show recipient and text, send only after the user says yes

wazap setup copies them into every client it connects, so there is usually nothing to run. The command behind it, for a harness setup never offered or for a checkout you want to install by hand:

npx wazap-mcp skills install codex     # or claude-code, cursor, agents

With no harness named it installs into every client it finds on this machine. For Claude Code the other route is the plugin, which carries the server as well:

/plugin marketplace add razvangirgiz/wazap
/plugin install wazap@wazap

Harness

Where the five directories land

claude-code

~/.claude/skills/

codex

~/.agents/skills/, the directory Codex documents for user skills. Cursor reads it too

cursor

~/.cursor/skills/

agents

./.agents/skills/, in the current project, for anything that reads the cross-tool convention

Re-running overwrites, so an upgrade is the same command. --dry-run lists what it would copy.

A client with no skills directory is not left out. The server registers each of the five as an MCP prompt of the same name, and sends a short instructions block that names all five and says when each applies, so an agent that never saw the skill files still follows them. That is how Claude Desktop, Gemini CLI and any client wired by hand get the workflows. A bridged session and a self-hosted HTTP server carry them the same way.

Errors

Every failure is a structured { error, message, fix } rather than a stack trace, so an agent can decide whether to retry, ask the user, or stop.

Code

Meaning

NOT_LINKED

No account linked. Call link_account, or run npx wazap-mcp login.

ALREADY_LINKED

link_account was called on a session that is already linked. Call get_status.

SESSION_EXPIRED

Unlinked from the phone. Run npx wazap-mcp login.

SESSION_CORRUPT

Credentials unreadable. Run npx wazap-mcp logout then login.

NOT_CONNECTED

Still connecting or reconnecting, or preparing the account database once after an upgrade.

SYNC_IN_PROGRESS

History sync has not finished; results may be partial.

INVALID_PHONE

Number is not in international format.

INVALID_ID

Not a WhatsApp chat, contact or group id.

NOT_ON_WHATSAPP

WhatsApp answered that the number has no account. A lookup it did not answer is NOT_CONNECTED.

CHAT_NOT_FOUND / MESSAGE_NOT_FOUND / CONTACT_NOT_FOUND / GROUP_NOT_FOUND

Unknown id.

NOT_A_PARTICIPANT / NOT_ADMIN / GROUP_ANNOUNCEMENT_ONLY

Group permissions.

MEDIA_UNAVAILABLE

WhatsApp expired the file, or it was never synced here.

FILE_NOT_FOUND / FILE_TOO_LARGE / URL_FETCH_FAILED / INVALID_IMAGE

Outbound media problems.

TEXT_TOO_LONG

Over WhatsApp's message limit.

EDIT_WINDOW_EXPIRED / RETRACT_WINDOW_EXPIRED / NOT_OWN_MESSAGE

WhatsApp's own limits on editing and deleting.

READ_ONLY

wazap is running read-only.

RATE_LIMITED

Too many writes; fix says how long to wait.

DRAFT_NOT_FOUND / DRAFT_EXPIRED

The draft is unknown, from another MCP session, sent more than 15 minutes ago, or expired unsent. Draft again.

SEND_OUTCOME_UNKNOWN

The message reached the socket and then the send failed, so WhatsApp may have it. The draft is never sent again; check the chat before drafting anew.

SEND_BLOCKED

The account's send rules refuse this recipient. wazap config send changes them; the agent must not route around.

AMBIGUOUS_ACCOUNT

More than one account could handle this, or a write named a chat no account knows. Pass account_id.

ACCOUNT_NOT_FOUND

No account with that id. Run wazap account add; get_status lists the ids.

ACCOUNT_DISABLED

That account is disabled. Run wazap account enable <id>; a running server picks it up.

TIMEOUT / WHATSAPP_ERROR

WhatsApp did not answer, or rejected the operation.

Data directory

Everything lives in ~/.wazap (override with --data-dir or WAZAP_DATA_DIR), created 0700 with credentials written 0600. A data dir from before several accounts moves into accounts/default/ the first time a wazap command runs.

~/.wazap/
  accounts.json     which accounts exist, and which is default
  accounts.json.required  empty marker: missing policy must not reset permissions
  accounts/<id>/
    auth/           WhatsApp credentials — treat this like a password
    media/          files saved by get_media
    wazap.sqlite    the account database: chats, contacts, messages, reactions,
                    receipts, transcripts, notes, recall vectors, deletion
                    barriers, the webhook outbox (plus -wal and -shm beside it)
    previews/       one small JPEG per photo or video already previewed
    qr.png          last QR, when login showed one
    legacy/         an earlier wazap's store.json, history/, retention.json,
                    notes.json and recall/, once imported; deleted a week later
    wazap.<time>.previous-owner.sqlite
                    the database a different number's link set aside; deleted a week later
  legacy/           the 0.15 beta archive.sqlite, once imported; deleted a week later
  models/           whisper.cpp and embedding models, when transcription or recall run locally
  server.lock       pid of the running server
  daemon.json       loopback endpoint a second wazap bridges to
  control.json      private loopback line the CLI uses to change the running server
  oauth.json        registered agents and hashed OAuth grants, when OAuth is on
  .env              optional settings, see .env.example
  migration.json    written once when a flat dir moved into accounts/default

Credential writes go to a temp file and are renamed into place, so killing the process mid-write cannot leave you re-linking your phone.

The account database

Each account keeps what it has seen in one SQLite file, wazap.sqlite (0600, in a 0700 folder), written with a full sync on every commit. Nothing of the history is held in memory: every read, search and restart goes to the file, and only small bounded caches stay in the process.

  • Upgrade. A data dir an earlier wazap wrote has store.json, history/, retention.json, notes.json, recall/ and perhaps the beta archive.sqlite. The first start imports them once, before the account is served; meanwhile its tools answer NOT_CONNECTED and get_status says it is preparing its database. A stop in the middle resumes where it left off at the next start. The import checks the database against what those files showed; a difference it cannot explain is logged by category and count, shown by wazap status, and the account is served from the database anyway. An unreadable retention.json stops the import, since history without its deletion barriers could bring deleted messages back. Once imported, the legacy files move into legacy/ and are never read again; see Upgrading to 0.22.

  • Logout deletes the credentials, and nothing else: the database stays, tied to the number, so the same number linking again finds its history. A different number linking sets the earlier database aside as wazap.<time>.previous-owner.sqlite and starts an empty one: one person's history never shows under another's, and legacy files the earlier number never imported stay for it. When the earlier number links again, its set-aside database comes back. A set-aside file nobody links is deleted a week later; WAZAP_RETENTION=1 does not shorten that week. wazap account remove stops the account, closes its database and deletes the whole folder with it.

  • WAZAP_PERSIST_HISTORY=0 removes every stored message at each start and stop, whatever WAZAP_RETENTION says, and with them every draft and the words of every send record; chats, contacts, notes, deletion barriers and the send records themselves stay, and recall is off.

  • wazap status reads each database read-only, with the server running or not: whether it is preparing (and the import phase), ready or imported with unexplained differences, its size, messages, chats and embedding queue, the legacy files and when they go, set-aside databases and the beta archive. --json carries the same as storage.

Upgrading to 0.22

0.22 moves each account from its files to the account database, once.

  1. The first start imports. Each account imports store.json, history/, retention.json, notes.json, recall/ and, for the number it is linked to, the 0.15 beta archive.sqlite. Until that is done the account's tools answer NOT_CONNECTED, and get_status and wazap status say it is preparing, with the phase it reached. A stop resumes at the next start. An account not linked at the upgrade imports the beta archive at the first start after its number links.

  2. The files move aside. Once imported, an account's files move into accounts/<id>/legacy/, and the beta archive into <data-dir>/legacy/ once every enabled account linked to its number has imported it. They are deleted a week after the move (wazap status shows the date), or at once with WAZAP_RETENTION=1. Only what wazap moved is deleted. An import whose check found differences it could not explain keeps its files until you delete them, and wazap status says how. A beta archive nobody linked to its number has imported stays where it is.

Rolling back to 0.21

A rollback trades what happened since the upgrade for the old files, and is only possible while those files exist.

  • What you lose on 0.21: every message, edit, reaction and note from the time you ran 0.22, and every deletion made then. Messages deleted while you ran 0.22 show again on 0.21, because its files predate the deletion.

  • When you cannot: with WAZAP_RETENTION=1 (the files were deleted at the upgrade), and once the week after the move is over. wazap status tells you whether legacy/ still exists.

In this order:

  1. Stop the server: wazap service stop, or stop the process that runs it.

  2. Back up the whole data dir (cp -a ~/.wazap ~/.wazap-backup) and keep that copy until you are sure.

  3. For each account, move everything in accounts/<id>/legacy/ back into accounts/<id>/. Leave wazap.sqlite where it is: 0.21 ignores it.

  4. Install 0.21.0 (npm i -g wazap-mcp@0.21.0) and start it.

To upgrade again later: stop the server, move wazap.sqlite with its -wal and -shm out of accounts/<id>/ (into your backup; do not delete it, it holds what arrived while you ran 0.22), put the beta archive back at <data-dir>/archive.sqlite if you want it imported again, and start 0.22. It imports the files as 0.21 left them. Never remove wazap.sqlite while part of the legacy files is still in legacy/ or already deleted: the next start would build the account from what is left.

Deleted and disappearing messages

An observed delete or revoke tombstones the message in the database in one transaction: its text, protobuf, transcript, reactions, votes, receipts and recall vector go, a reply quoting it loses the quoted copy, and reads stop showing it at once. Its preview file is removed through a queue kept in the database, so a crash between the two finishes the removal at the next start. Successful delete/clear tools wait for that cleanup; a disk failure is reported even if WhatsApp already accepted the deletion. Pending preview/transcription results cannot restore a deleted message. A revoke only ever removes a message in the chat it arrived in. Chat metadata never keeps an embedded copy of a message.

The tombstones and each chat's clear time are the barriers: message IDs and times, no bodies. They keep replay and backfill from resurrecting a deleted message, and they remain even with WAZAP_PERSIST_HISTORY=0. Clearing a chat hides it at once and purges it in chunks that resume after a crash; backfill dated at or before the local clear time is refused. WhatsApp timestamps have second precision, so a message in the same second can be suppressed. A database that cannot be opened keeps the account from being served rather than replay deleted messages.

Strict retention (WAZAP_RETENTION=1, off by default)

Without it, wazap keeps what it has seen, as before 0.21: disappearing-message timers are not enforced locally. With it:

  • For messages carrying disappearing-message metadata, wazap keeps the earliest observed deadline across edits, aliases, backfill and restarts. Reads refuse the message at that instant; one background timer per account tombstones it in the database the way a deletion does. Preview/transcription results, forwards, quoted replies and queued/retried webhooks recheck retention before publication. Already-started operations cannot be recalled.

  • The policy is conservative: a marked ephemeral message without a computable deadline is refused, and keep-in-chat hints are not an indefinite exemption. Current chat settings are not retroactively applied to unmarked messages. Keep the system clock synchronized.

  • A text-only row carried over from an older recall index keeps the deadline the legacy files knew for it; one they knew none for stays.

The database overwrites what it deletes (secure_delete), but this is not secure erasure of heap pages, backups or filesystem snapshots. Explicit exports, independent quotes/forwards and data already returned or sent are not recalled. While wazap is stopped or suspended, disk cleanup waits until it runs again. Old unrecorded deletions cannot be reconstructed. See the audit report.

Several accounts

One wazap serve holds every enabled account in the data dir. Each account is its own Baileys socket and its own folder under accounts/<id>/. The first account is default. Add another with wazap account add work --name Work, then wazap login --account work.

--account picks one on login, logout, status, config writes and webhook test. MCP tools take an optional account_id; get_status lists every account when more than one is linked. A chat only one account knows selects that account. A send to a chat no account knows, with two or more accounts, fails AMBIGUOUS_ACCOUNT instead of falling back to default.

Reads without a chat and without account_id use the default account; the response still carries account_id. find_contact is the exception: without account_id it searches every account and labels what it finds. link_account needs an account that already exists. Five accounts is advice, not a cap. One phone number is one account.

A running server follows the registry; there is nothing to restart. wazap account add, enable, disable, default and remove tell it at once: an added or enabled account gets its socket, a disabled one is stopped and tools that name it answer ACCOUNT_DISABLED, a removed one is stopped before its folder is deleted. A tool that names an account the server has not seen yet reads accounts.json again before answering ACCOUNT_NOT_FOUND, so account add followed by link_account works even when nothing told the server. The last account a server runs stays up until the server stops, because a server with no enabled account refuses to start: account disable says so, and account remove refuses it.

wazap logout --account work unlinks one account. With a server running it asks that server to do it: the account's socket closes (a pairing in flight is cancelled), WhatsApp is told to unlink the device, its credentials and chat snapshot are deleted, and the account stays in the registry, not linked, ready for link_account or login. The other accounts are not touched. Without --account, logout is for the default account. With no server running, logout does the same work itself.

The CLI reaches the running server over a private line, not the MCP endpoint: a listener on an ephemeral 127.0.0.1 port and a random token, both written to <data-dir>/control.json (0600) by the server as it starts. Nothing else opens it — not WAZAP_READ_TOKEN or WAZAP_WRITE_TOKEN, not an OAuth grant, not the bridge token in daemon.json, not an anonymous caller — and no tool exposes it to an agent. A tunnel or proxy pointed at WAZAP_PORT never reaches it. It is there whether or not the session is shared. A server started by an older wazap has no such line: against it, account changes print a restart hint, and logout and account remove work as they used to (stop the wazap service around the logout, or refuse while another server runs).

An account can override the global webhook URL, secret and event list in accounts.json (webhook_url, webhook_secret, webhook_events). wazap webhook test --account work posts with that account's id and name.

Several clients at once

Claude Desktop, Claude Code and Cursor each launch their own wazap. The first one on a data directory owns every enabled account, each on its own socket, and opens an MCP endpoint on 127.0.0.1; every later one bridges to it over that endpoint. There is nothing to configure, and no client can tell the difference. The owner publishes <data-dir>/daemon.json (0600) with its pid, its port and the token a bridge authenticates with.

A bridge serves whatever the owner exposes, so an owner started --read-only makes every client read-only, whatever flags that client was launched with.

When the owner exits, the bridges exit with it, and the next wazap a client starts becomes the new owner.

An explicit --http is a server of its own rather than a bridge: a second one on the same directory exits with code 2 naming the pid of the one already running.

Read-only mode

Writes are opt-in at login (the question defaults to no and stores the answer in <data-dir>/.env). wazap config writes on|off changes it later. wazap config and wazap status print the effective setting and where it came from. If that line says off, write tools are not registered: run wazap config writes on and restart the server.

An unset WAZAP_READ_ONLY and WAZAP_READ_ONLY=0 both register write tools (wazap config then says "writes: on"). WAZAP_READ_ONLY=1 or wazap serve --read-only does not register them at all. The agent never sees them, so it cannot message anyone from your number even by mistake.

Writes are also rate limited to 20 a minute per account, or the account's rate_limit in accounts.json. Sending faster than a human is how accounts get banned.

Text sent or edited by wazap goes out without a link preview, and nothing fetches the page: not wazap, and not Baileys, whose own fetcher is kept off explicitly on every send, confirm and edit. The link itself arrives intact, as text. Forwarding an existing message may keep the preview already embedded in it, without fetching it again.

Photo previews are decoded locally. Video frames, outgoing video thumbnails, GIF conversion and local transcription use restricted ffmpeg inputs: local-file protocol only, with a media-format allowlist that excludes playlists and image sequences. GIF conversion also requires the GIF demuxer. An unavailable video thumbnail does not fall back to Baileys's unrestricted ffmpeg command.

These checks are not a codec sandbox. Keep ffmpeg and image decoders updated; exotic formats outside the allowlist may no longer work. Decoder errors omit raw stderr, which can contain untrusted metadata or private content.

Send rules

An account can also be limited in who it may message — the case where the agent may send, but only to the people you run it for. The rules live on the account record in accounts.json and are edited per account with wazap config send (--account picks which; run without a verb to print them). Entries are chat ids or numbers in international format, comma-separated:

  • wazap config send deny 40722123456,120363000000000001@g.us refuses those recipients, whatever else is allowed.

  • wazap config send allow +15550100,40722123456 makes the list exhaustive — only those may be messaged. allow none locks the account to nobody.

  • wazap config send open lifts every restriction.

The send tools check the rules when a message is drafted and again at confirm_send, so a rule written while a draft waits still applies to it. A refused send fails SEND_BLOCKED naming the rule that fired; the agent is told to tell you, not to retry or route around it.

Saving policy or starting the account hub writes an empty 0600 accounts.json.required marker. If that known policy disappears, wazap refuses unrestricted defaults. Restore the policy from a trusted backup. To start over on purpose with one default account and no send rules, delete the marker too; never do it just to get past the error. Fresh/legacy directories without a marker keep their bootstrap behavior. Explicit layout rollback removes the modern registry and marker; old flat-layout versions do not enforce per-account rules.

Every write tool checks the current disk policy before preparing work. Corrupt, missing, disabled or newly read-only accounts refuse the write without consuming an owned draft. Cached rules are not a fallback. The running account roster follows the registry (see Several accounts); relaxing startup read-only settings still needs a restart, and these checks cannot undo an already-started operation. A registry that is missing or malformed is never applied: the running roster stays as it was and the calls that need it fail. Malformed WAZAP_READ_ONLY values are refused. Unset global settings still mean writes on; for a durable account-level prohibition use wazap config writes off --account <id>.

HTTP mode

npx wazap-mcp serve --http

Streamable HTTP at /mcp on 127.0.0.1:8766 (--host and --port, or WAZAP_HOST and WAZAP_PORT), with a health check at /healthz. That check answers { ok, status, since }; the list of accounts and their status needs a credential. It turns 503 once the socket has been anything but connected for two minutes, so a tunnel or a monitor sees a real outage rather than a reconnect in progress.

An agent reaches it by URL and signs in with OAuth; wazap expose sets that up. A product calling wazap from its own code uses a static token instead: see Building on wazap. wazap refuses to bind a non-loopback address without a read token.

Client isolation

HTTP MCP sessions are bound to the exact bearer credential that initialized them (stored in memory as a SHA-256 fingerprint), including its read/write permissions. Another credential cannot use that session's POST, GET or DELETE endpoint, even if it knows the session id; it receives 404 Session not found. Each request still validates the token, so expiry or revocation returns 401. After an OAuth access token rotates, initialize a new MCP session when the old session returns 404.

Clients sharing a static token share an identity, and unauthenticated readers share an anonymous identity. Use distinct credentials/OAuth grants for isolation; do not enable anonymous access on a sensitive endpoint. This is not per-client account isolation: authorized clients still share account data and account-level policies.

Drafts are owned by the MCP session that created them, including stdio servers and each bridge's upstream session. Another session cannot confirm a draft even if it knows its id; it receives DRAFT_NOT_FOUND, without consuming the draft. Resuming the same authenticated session preserves its drafts. A new initialize (after eviction, reconnect with a new session, or OAuth token rotation) requires a new draft and fresh user approval. Two sessions using the same token have separate drafts, but sharing that token is still sharing an identity: anyone holding both that token and the owner's session id can act as that session. Draft/confirm is a workflow, not independent proof of human consent; the agent can call both tools unless a trusted harness enforces approval.

Request budgets

MCP POSTs authenticate before JSON parsing, accept at most 100 KiB and refuse compressed bodies. Each credential has 240 POSTs/minute across its sessions; 429 responses include Retry-After. The session registry holds at most 128 sessions overall and 32 per credential, evicting that credential's oldest first. Tool work is capped at eight concurrent operations per MCP session and 32 across the process, including stdio/bridges. Slots remain held until work settles, not merely until a client disconnects. Retry once after pending work completes.

The HTTP listener caps connections at 256, header receipt at ten seconds and request-body receipt at thirty seconds; this does not time out legitimate SSE streams or long-running tools. Anonymous loopback requests must have a loopback Host name (on any port, so a mapped container port works) and, when supplied, a matching Origin; browser rebinding/cross-origin requests are refused. Always configure credentials for a proxy/tunnel. These bounds are not a DDoS shield or per-tenant fairness guarantee.

Host files and remote media

HTTP clients — static read/write tokens and OAuth grants — cannot use file_path or override get_media's directory with save_to. This applies even on loopback: a reverse proxy or tunnel also reaches the server from localhost. The tools return MEDIA_ACCESS_DENIED before looking up a path or touching a file. A write token grants WhatsApp writes, not access to the host filesystem.

Remote clients can use public HTTP(S) media URLs, forward existing WhatsApp messages, and download attachments into the account's default media directory. Small downloaded images still return inline. Other attachments are saved on the server; there is no arbitrary-file upload/download endpoint.

Local stdio clients retain local file access. A local bridge gets it only through the private daemon credential stored in daemon.json, never through a public read/write token or OAuth grant. Keep that credential private; it is a local filesystem capability as well as a WhatsApp credential. No client-provided flag or argument enables it.

URL media fetches check every DNS answer and redirect, pin the validated address for the connection, check the peer address, and cap response size. Fetch errors do not echo signed URLs. HTTP request logs omit queries, arbitrary URL paths, request bodies and raw Accept headers; malformed-request errors are sanitized. Client labels are bounded and stripped of log/terminal control characters. Do not place secrets in client names or User-Agent labels, which remain diagnostic metadata.

Self-host

Run wazap on a server of your own when the agent is not on your laptop: another machine, a VPS, a client's infrastructure. The session stays on that server; nothing goes through a third party.

With systemd

npm install -g wazap-mcp
sudo useradd --system --home /var/lib/wazap --create-home wazap
sudo -u wazap WAZAP_DATA_DIR=/var/lib/wazap wazap login --phone +15550100   # pairing code works over SSH
sudo -u wazap tee /var/lib/wazap/.env >/dev/null <<END
WAZAP_READ_TOKEN=$(openssl rand -hex 32)
WAZAP_WRITE_TOKEN=$(openssl rand -hex 32)
END
sudo curl -fsSL https://raw.githubusercontent.com/razvangirgiz/wazap/main/deploy/wazap.service -o /etc/systemd/system/wazap.service
sudo systemctl enable --now wazap
curl -s http://127.0.0.1:8766/healthz

The unit binds loopback only. Put TLS in front with the two-line deploy/Caddyfile (caddy run --config deploy/Caddyfile after editing the hostname) or any reverse proxy, then turn on OAuth and give agents https://your-host/mcp. The tokens are for your own code.

With Docker

git clone https://github.com/razvangirgiz/wazap && cd wazap
printf 'WAZAP_READ_TOKEN=%s\nWAZAP_WRITE_TOKEN=%s\n' $(openssl rand -hex 32) $(openssl rand -hex 32) > .env
docker compose run --rm wazap login --phone +15550100   # once; the session lands in the wazap-data volume
docker compose up -d
curl -s http://127.0.0.1:8766/healthz

The container publishes 8766 on loopback only; add the same TLS proxy in front. Upgrading is git pull && docker compose up -d --build; the volume keeps the session.

A proxy on the host reaches the container through the published port, so inside the container its address is the compose network's gateway, not loopback. The compose file pins that network to 172.30.87.0/24 and trusts its gateway, 172.30.87.1, for X-Forwarded-For; without that, every OAuth caller would share one password lockout. If the subnet clashes with one of yours, change both together, or set WAZAP_TRUST_PROXY in .env.

From a machine without a public address

A laptop or a box behind NAT can still serve hosted agents through a tunnel, with no port opened and TLS done at the edge. npx wazap-mcp expose does the whole thing with Tailscale Funnel or Cloudflare Tunnel, whichever is installed. See Keep it running.

wazap keeps binding loopback either way; only the tunnel reaches it.

cloudflared tunnel login
cloudflared tunnel create wazap
cloudflared tunnel route dns wazap wazap.example.com
cloudflared tunnel run --url http://127.0.0.1:8766 wazap

Set WAZAP_PUBLIC_URL=https://wazap.example.com for OAuth and keep cloudflared running the way you keep wazap running (a systemd unit, a launchd agent). Tailscale Funnel or ngrok work the same way: whatever ends at https://your-host with /mcp behind it.

Which clients can reach it

Agents sign in with OAuth, the next section: claude.ai Connectors, ChatGPT, Poke and any MCP client that signs in to a URL. Your own code calling wazap uses a static token instead; see Building on wazap.

Reverse proxy trust

With OAuth enabled, WAZAP_TRUST_PROXY controls which peers may supply X-Forwarded-For for password lockouts and request limits. The default is loopback, not every private or Docker address. Set it to none for direct connections without a proxy, or to a comma-separated list of exact proxy IPs or CIDRs, for example loopback,172.20.0.2/32. Restart after changing it. Do not trust a whole LAN just because the proxy runs there.

The trusted proxy must remove or sanitize incoming X-Forwarded-For and append the actual client address. CF-Connecting-IP alone is ignored; configure a Cloudflare/tunnel ingress to produce a trustworthy X-Forwarded-For chain. Otherwise callers behind that ingress share the proxy's rate-limit identity. Check the real header chain before exposing the service. Proxy trust does not grant authentication, write permissions, or local-file access.

Hosted agents (OAuth)

Two more lines in the same .env turn wazap into its own OAuth 2.1 server:

WAZAP_PUBLIC_URL=https://wazap.example.com
WAZAP_OAUTH_PASSWORD=$(openssl rand -base64 18)

Then give an agent nothing but https://wazap.example.com/mcp. It finds the authorization server at /.well-known/oauth-protected-resource/mcp, registers itself (RFC 7591, so there is no client id to paste anywhere), and sends you to a page on your own host that asks two things: the password above, and whether this agent may only read or also send. A refresh token keeps the agent signed in until you revoke it; access tokens rotate every 24 hours on their own.

Tested against the flow claude.ai, ChatGPT and Poke use: S256 PKCE, public clients, /token with refresh, /revoke. The bearer tokens keep working next to it, so a laptop client on a header and a hosted agent on OAuth share one server.

What to know before exposing it:

  • WAZAP_PUBLIC_URL must be https and a bare origin, no path: the endpoints live at its root. The password travels to it.

  • The password is the whole identity layer. Use a long one. A consent page takes three wrong guesses and is gone; five from one address lock that address out for fifteen minutes; twenty from anywhere pause consent for everyone for one minute.

  • With OAuth on, /mcp never answers an unauthenticated request, whether or not a read token is set.

  • Grants live in <data-dir>/oauth.json as hashes. Delete the file to sign every agent out at once, running server included; wazap status lists who holds one. Disconnecting an agent on its side revokes its refresh token and every access token in that grant family. Refresh tokens rotate on every use: clients must save the returned token. A consumed token still works for 60 seconds from its rotation, so two refreshes at once or a lost response do not sign the agent out; replaying it later, within the last 32 consumed tokens, revokes the family, and older tokens are simply invalid. At most eight access tokens per grant remain active. A refresh token unused for ninety days is dropped. Damaged persisted grants require sign-in again rather than becoming unexpiring.

  • A read grant never sees a write tool, whatever scope the agent requested. The radio button on the consent page is the only thing that decides. Refresh requests may narrow scopes; asking for more gets the grant's scopes, and a request with none of them is rejected.

  • A supplied OAuth resource must be this server's exact MCP URL, including /mcp. A different path, origin, query or fragment is rejected at authorization, code exchange and refresh. Older clients may omit resource.

  • Write tools may be visible because one linked account allows them. Each write still checks the resolved account's read-only policy before draft creation or media preparation, as well as the existing service-level send check. This is account policy enforcement, not per-client account ACLs; policy changes still require the documented server restart.

Building on wazap (HTTP API for products)

A product that drives WhatsApp from its own code, such as a booking app that answers its customers, talks to wazap serve --http with static bearer tokens rather than a person signing in. An agent does not need this: it uses a local client or OAuth.

WAZAP_READ_TOKEN=$(openssl rand -hex 32) \
WAZAP_WRITE_TOKEN=$(openssl rand -hex 32) \
npx wazap-mcp serve --http --host 127.0.0.1 --port 8766

Keep both in <data-dir>/.env rather than on a command line. Every request to /mcp carries Authorization: Bearer <token>:

  • WAZAP_READ_TOKEN gets the read tools. Without it, and without OAuth, /mcp answers any process that reaches it: wazap refuses to bind a non-loopback address without it, and to serve a port a tunnel points at without it or OAuth.

  • WAZAP_WRITE_TOKEN can also unlock the write tools. A leaked read token can never message anyone.

A write token is not the same as writes being enabled: when the server or the account is read-only, even a write-token session has no write tools, and get_status answers write_tools: false with the fix (wazap config writes on, then restart). A read token never registers write tools.

Sessions on the same token share an identity; see Client isolation. A token never grants host files; see Host files and remote media. What happens on WhatsApp comes back through the outbound webhook, signed, so the product does not poll.

Outbound webhook

Live events POST to one URL. Off by default. History sync is not posted. Only message_received is posted unless you ask for more, because a consumer that answers every POST without reading event would otherwise answer the messages its own owner typed on the phone.

The event field names one of three. message_received is a message another person sent. message_sent is a message this account sent itself, typed on the phone or on another linked device; a message wazap sent through its own tools is not announced, so a consumer can never be made to answer itself. connection says the link came up, went down or expired.

Ask for the other two in WAZAP_WEBHOOK_EVENTS, comma-separated (message_received,connection), or say all for the three of them. Case does not matter and the spaces around a name are ignored. An unknown name fails wazap status, doctor and setup. An account carries its own list as webhook_events in accounts.json.

npx wazap-mcp config webhook on    # asks for URL + secret (secret is not echoed)
npx wazap-mcp webhook test         # POST a probe event
npx wazap-mcp webhook test --event connection   # needs connection enabled
npx wazap-mcp webhook test --account work
npx wazap-mcp config webhook off

On without a URL or secret fails wazap status, doctor and setup. A failed delivery never stops WhatsApp or MCP.

Events wait in an outbox inside the account database, written in the same transaction as the message they announce, so a restart or a crash loses none. They are posted one POST at a time per account, each bounded by 10 seconds. A timeout, an unreachable URL, or a 408, 425, 429 or 5xx is retried after 1 s, 5 s, 30 s and 2 min, then every 5 min, and one last time 24 hours after the event; then it has failed. A new event, or a POST that gets through, retries at once every waiting event last tried 30 s ago or more, so a receiver that comes back hears the backlog within moments. Any other 4xx, such as the 401 of a receiver whose API key changed, is a refusal: the event is posted once and fails, and the error names the status with a hint. Either way the failure sets webhook.last_error, which the next delivery clears. An event is posted at least once: a POST a crash interrupted is sent again, so dedupe on message_id. Turning the webhook off, or dropping an event from WAZAP_WEBHOOK_EVENTS, cancels what waits; nothing is queued while it is off. With WAZAP_PERSIST_HISTORY=0 the stored messages are removed at every stop and start, so a message event still waiting then is cancelled rather than posted after the restart; connection events still go out.

A message event is built when it is posted, from the message as it is then: an edit or a transcript that arrived in the meantime goes with it, and a message deleted, expired or cleared first is not posted at all.

Redirects are never followed. Response bodies are cancelled without being read, including successful ones. Diagnostics retain the destination host, status and failure category, not URL paths/queries, response bodies or transport exception excerpts. URL query tokens remain supported, but userinfo credentials and fragments are refused. Webhook and transcription destinations are trusted operator configuration, not agent-supplied public-media URLs: configure only receivers allowed to see this account's data.

get_status counts the outbox's events in webhook.delivery: delivered (kept 7 days), failed and cancelled (kept 30 days; a retried event that never got through counts once), pending, dropped (events the account database could not store), consecutive_failures, retrying (failed POSTs of the oldest waiting event), last_success_at, last_failure_at, last_failure, last_status, last_dropped_at and oldest_pending_at. wazap status and doctor read the same outbox read-only, whether or not the server runs: three failed events in a row, or an oldest waiting event still retried 10 minutes after it was queued, fail the webhook check with the fix; one failure since the last delivery, or an event retried for less, warns; the check passes again with the next delivery. webhook test posts its probe directly, not through the outbox, and does not change the counters. The log says the first failure of a run, a count every 100 failures, and one line when delivery comes back, not a line per event.

An account may set webhook_url, webhook_secret and webhook_events in accounts.json; those win over the global URL, secret and event list, and config webhook off --account work clears all three.

webhook test --event <name> posts nothing and exits non-zero when that event is not enabled, and says what to enable it with. While the webhook is on, wazap config prints the events it posts on an events: line.

HMAC: X-Wazap-Signature is sha256=<hex>, HMAC-SHA256 of the exact raw JSON body with the secret that signed it. Verify that raw body, not a re-serialized object. HTTPS only, except http:// on loopback.

contact_id is the sender's contact in the account database: the same number for the same person however WhatsApp spells their id, including after the number behind a @lid becomes known. phone is the sender's number in E.164 (+15550100), or null while WhatsApp has not revealed it; from keeps its old form. For message_sent both name the account itself.

text is a preview, cut at 2000 characters and ending in a single . truncated is true when it was cut. For kind: "audio", text is the transcription when wazap auto-transcribed the note itself, which it does for incoming notes only; the event waits up to 60 seconds for those words, a restart included, and goes at once when the transcription fails or the provider is paused. In every other case text is the [voice message · 0:42] placeholder: a note you recorded yourself, a note longer than 600 seconds, a note WhatsApp stated no duration for, and a transcription that failed every attempt or did not finish within the 60 seconds. ts is the original local time with a numeric offset, kept for consumers already reading it, and timestamp is the same instant in UTC.

Within a chat, events arrive in the order wazap queued them: nothing of a chat is posted while an older event of that chat waits for its transcript or its next retry. Across chats there is no order: a chat whose event is retried, or a voice note waiting for its words, holds back no other chat. connection events keep their own order. Delivery is at least once, so dedupe on message_id, and order by timestamp when it matters, since WhatsApp itself can deliver a message late.

A message another person sent:

{
  "event": "message_received",
  "from": "15550100",
  "contact_id": 42,
  "phone": "+15550100",
  "chat_id": "15550100@s.whatsapp.net",
  "ts": "2026-09-08T17:00:00+03:00",
  "timestamp": "2026-09-08T14:00:00.000Z",
  "text": "hello, or a preview of something longer",
  "truncated": false,
  "kind": "text",
  "from_me": false,
  "is_self_chat": false,
  "message_id": "false_15550100@s.whatsapp.net_3EB0…",
  "account_id": "default",
  "account_name": "default"
}

A message sent from the phone, here in the "Message yourself" chat:

{
  "event": "message_sent",
  "from": "15551234",
  "contact_id": 1,
  "phone": "+15551234",
  "chat_id": "15551234@s.whatsapp.net",
  "ts": "2026-09-08T17:04:12+03:00",
  "timestamp": "2026-09-08T14:04:12.000Z",
  "text": "call the notary at 14:00 on Wednesday",
  "truncated": false,
  "kind": "text",
  "from_me": true,
  "is_self_chat": true,
  "message_id": "true_15551234@s.whatsapp.net_3EB0…",
  "account_id": "default",
  "account_name": "default"
}

A connection change. status is linked, disconnected or expired; not_linked, linking and connecting post nothing, and two changes that mean the same status post once.

{
  "event": "connection",
  "status": "expired",
  "timestamp": "2026-09-08T14:10:00.000Z",
  "account_id": "default",
  "account_name": "default"
}

connection reports what the socket does while wazap is running. A clean shutdown posts nothing, and a crash posts nothing either, so silence does not mean the link is up. Poll get_status when you need to know that. A connection event the receiver did not take is retried like any other, until a newer change is queued behind it: then it is cancelled, so after an outage the receiver hears the current status once, not every flap.

Settings

Most of these are written for you by wazap config, wazap login and wazap expose; .env.example shows them all with their comments.

Variable

Default

Meaning

WAZAP_DATA_DIR

~/.wazap

Where everything is stored.

WAZAP_READ_ONLY

unset (0)

1 does not register the write tools. Unset and 0 both do. wazap config writes on|off sets it.

WAZAP_PERSIST_HISTORY

1

Privacy: 0 keeps no messages on disk, removing them at each start and stop; barriers, chats, contacts and notes stay, and recall is off.

WAZAP_HOST / WAZAP_PORT

127.0.0.1 / 8766

Where wazap serve --http listens.

WAZAP_PUBLIC_URL / WAZAP_OAUTH_PASSWORD

unset

The https address agents reach the server at, and the password its consent page asks for (at least 8 characters). Both together turn OAuth on; wazap expose sets them.

WAZAP_READ_TOKEN / WAZAP_WRITE_TOKEN

unset

Static bearer tokens for your own code; see Building on wazap.

WAZAP_TRANSCRIBE

off

local, openai or off. wazap config transcribe sets it.

WAZAP_TRANSCRIBE_API_KEY

unset

The key for openai; OPENAI_API_KEY is the fallback. Never a flag.

WAZAP_RECALL

off

local turns on semantic recall. wazap config recall sets it.

WAZAP_WEBHOOK

off

on posts the enabled events to the webhook URL. wazap config webhook sets it with the next two.

WAZAP_WEBHOOK_URL / WAZAP_WEBHOOK_SECRET

unset

HTTPS endpoint (http:// only on loopback) and the shared secret for X-Wazap-Signature. The secret is never a flag. An account's webhook_url and webhook_secret win.

WAZAP_WEBHOOK_EVENTS

unset (message_received)

Which events to post, comma-separated, or all. An account's webhook_events wins.

WAZAP_RETENTION

0

1 turns on strict retention.

WAZAP_TRUST_PROXY

loopback

Advanced, for a self-hosted OAuth server behind a proxy: the proxy IPs/CIDRs trusted for X-Forwarded-For, comma-separated; none trusts no proxy.

Flags beat environment variables, which beat <data-dir>/.env. A setting an earlier wazap read and this one does not is ignored, with a warning at startup that says what replaced it; see the CHANGELOG. WAZAP_TRANSPORT=http still serves HTTP through 1.x, with a warning: --http is the supported way, and the variable goes away in 2.0.

Known limitations

  • Unofficial. Baileys reverse-engineers the WhatsApp multi-device protocol. This is not the WhatsApp Business API and Meta does not support it.

  • Ban risk is real. Automated sending, bulk messaging or anything a human would not plausibly type can get the number banned, and that is not recoverable from here. The rate limit helps; it is not a guarantee.

  • Media keys expire. WhatsApp drops old attachments from its servers, so get_media on an old message returns MEDIA_UNAVAILABLE.

  • History is what the phone syncs. wazap sees the history WhatsApp hands the linked device, not your full phone archive. read_messages with before asks for more, within whatever WhatsApp still keeps.

  • @lid ids. Newer accounts are addressed by a privacy id rather than a phone number. wazap translates them back to phone numbers when it has learned the mapping, and passes the @lid through when it has not.

  • Names come from the phone's address book. WhatsApp delivers it as an app state sync, and only to a connection asking for it from scratch. If contacts read as phone numbers and get_status shows contacts_named: 0, find_contact asks for it once, and wazap contacts resync asks again while no server runs.

  • Calls are WhatsApp calls only. A call shows up as a message with type: "call", carrying its kind, direction, outcome and duration. WhatsApp's own call log and the missed-call notices arrive on their own; a call that starts and ends while wazap is running is recorded live, so calls placed or received while it is stopped can be missing entirely. A cellular call from the phone's dialler is never visible, on any device.

  • Your phone must stay reachable. A linked device stops receiving once the phone has been offline long enough; get_status says so in hint.

Development

npm install
npm run typecheck
npm test                       # builds, then runs node --test
node test/smoke-stdio.mjs      # drives the built binary over MCP stdio
npm run dev -- status          # run from source with tsx

npm test needs no WhatsApp session. The stdio smoke test spawns the built binary against a throwaway data directory and checks that an unlinked install still answers initialize, tools/list and get_status.

MIT licensed.

Available Tools

38 tools
confirm_sendSend a drafted WhatsApp messageA

Send a draft created by send_message, send_media, send_poll, send_location or forward_message. This is the only call that reaches WhatsApp, and it sends a draft at most once: confirming it again answers the same receipt with already_sent: true. Only the MCP session that created the draft may confirm it. After reinitializing or reconnecting with a new session, draft again and obtain fresh user approval. A missing or expired draft_id also means draft again and show the new preview before calling this. SEND_OUTCOME_UNKNOWN means WhatsApp may have the message: check the chat with read_messages before anything else, and never draft it again without asking the user.

ParametersJSON Schema
NameRequiredDescriptionDefault
draft_idYesThe draft_id returned by a send_* tool
account_idNoRegistry account id (default, work, …). Omit to resolve from chat_id or message_id, or the default account.

TDQS

A4.9/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Discloses critical behavior beyond annotations: at-most-once sending, already_sent responses, session ownership, the need for fresh user approval after reconnecting, and SEND_OUTCOME_UNKNOWN handling. This aligns with openWorldHint=true and idempotentHint=false and adds substantial context.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Every sentence carries operational weight. The description is front-loaded with the purpose, then efficiently covers idempotency, session constraints, failure recovery, and unknown outcomes. No filler or redundant restatement of the schema.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given there is no output schema, the description fully covers what the agent needs to invoke this safely: success semantics, idempotent re-confirmation, session validity, recovery from missing/expired drafts, and handling of SEND_OUTCOME_UNKNOWN. It is complete for a side-effect-heavy tool.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so the baseline is 3. The description adds meaning beyond the schema by explaining that draft_id is session-scoped, single-use, and can expire, which directly affects how the agent should treat the parameter. account_id is already well documented in the schema.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and resource: 'Send a draft created by send_message, send_media, send_poll, send_location or forward_message.' It also distinguishes itself from siblings by declaring it is the only call that reaches WhatsApp, making its role clear.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Provides explicit when-to-use and when-not-to-use guidance: only after drafting, only within the creating session, and never when draft_id is missing/expired without re-drafting and showing a preview. It also names read_messages as the check for SEND_OUTCOME_UNKNOWN, giving a concrete alternative action.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

create_groupCreate a WhatsApp groupA

Create a group with the given name and participants; the linked account becomes the owner. Each participant comes back with a status: ok, invite_needed (their privacy settings require an invite link) or failed.

ParametersJSON Schema
NameRequiredDescriptionDefault
nameYesGroup name
account_idNoRegistry account id (default, work, …). Omit to resolve from chat_id or message_id, or the default account.
participant_idsYesChat ids or phone numbers to add (1-256)

TDQS

A4/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnly=false, idempotent=false, destructive=false, and openWorld=true, so the safety profile is covered. The description adds genuinely useful behavior beyond that: the per-participant status vocabulary (ok, invite_needed, failed) tells the agent to expect partial success rather than all-or-nothing.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Two sentences with no filler; the core action and required inputs come first, and the return-status detail is efficiently packed into the second sentence.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a 3-parameter mutation tool with no output schema, the description covers the action, required inputs, ownership effect, and return statuses. It stops short of explaining failure handling (e.g., what happens if every participant fails) or limits, but the essentials are present.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the schema already documents name, account_id, and participant_ids with constraints and defaults. The description only restates 'name and participants' and adds no format or resolution detail beyond the schema, so baseline 3 applies.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and resource ('Create a group'), names the required inputs ('given name and participants'), and adds the ownership outcome ('the linked account becomes the owner'), which cleanly separates it from read-oriented siblings like get_group_info and manage_group.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The creation context is implied by the verb, but there is no guidance on when to use this versus manage_group, nor any prerequisites (e.g., the account must be linked) or exclusions. Adequate but leaves the agent to infer routing.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

delete_messageDelete a WhatsApp messageA
Destructive

Delete a message. DESTRUCTIVE — confirm with the user first. for_everyone is required, and picks one of two different deletes; tell the user which:

  • for_everyone: true retracts it for everyone in the chat. Works on messages the linked account sent, within 2 days of sending. In a group where the linked account is an admin it also takes someone else's message, deleted as an admin; anywhere else that is NOT_OWN_MESSAGE.

  • for_everyone: false removes it from the linked account's own devices only: anyone's message, at any age. Nobody else sees a change.

ParametersJSON Schema
NameRequiredDescriptionDefault
account_idNoRegistry account id (default, work, …). Omit to resolve from chat_id or message_id, or the default account.
message_idYesMessage id from read_messages / search_messages / get_message, e.g. "false_4072...@s.whatsapp.net_3EB0..."
for_everyoneYesRequired. true retracts it for everyone in the chat; false deletes it for the linked account only

TDQS

A4.4/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Beyond the destructiveHint annotation, the description discloses the exact consequences of each deletion mode, including whether the change is visible to others, the 2-day time window, and the NOT_OWN_MESSAGE error condition. This gives the agent a thorough understanding of what will actually happen, which is especially important for a destructive operation.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is front-loaded with the key warning "DESTRUCTIVE — confirm with the user first," followed by a well-structured bullet list. Every sentence earns its place, and the two alternatives are presented in a scannable, unambiguous format.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a destructive tool with multiple behavioral subtleties, this description covers the essential scenarios: own messages, admin deletions, non-own messages, device-only deletion, and time limits. The optional account_id and source of message_id are covered in the schema, so nothing critical is missing for correct invocation.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The input schema already documents all parameters with 100% coverage, so the baseline is 3. The description adds meaningful semantics beyond the schema by explaining the limitations and side effects of for_everyone true versus false in detail. It does not add much for account_id, but the schema description covers that sufficiently.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool's function with a specific verb and resource: "Delete a message." It also explains the two deletion modes, making the core purpose unmistakable. However, it does not explicitly differentiate itself from the sibling tool 'recall', which may also perform message deletion.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides clear context for when to use each for_everyone setting, including time limits, message ownership, and admin capabilities. It also instructs the agent to confirm with the user before proceeding. It does not explicitly list alternative tools for similar operations, so it stops short of full when/when-not guidance.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

download_mediaDownload media from a WhatsApp messageA
Read-onlyIdempotent

Download the photo/video/audio/document attached to a message and save it to disk on the machine running wazap. The file at path is already decrypted — open or process it as is; nothing else is needed. Images of 1 MB or less are also returned inline so you can look at them.

The structured result carries: path (the saved file), mime, size (bytes), filename (the name it was saved under — a timestamped name wazap made, not the sender's), original_filename (the name the sender's file had, or null when the envelope carried none), caption (the text the sender wrote under the media, or null — audio and voice notes cannot carry one), message_id and sender (the same identity fields as search_messages — is_saved, contact_name, pushname, name_source — or null when even the message can no longer be read back).

The id resolves in its raw form too: false_<lid>@lid_<stanza> works whether or not the chat's number was ever learned — when it was, the lid spelling finds the same message filed under the paired number. An unresolved sender never blocks the file. Without account_id the same lookup walks every linked account's store in turn before failing, and the result's account_id names the one that served the file.

Fails with MEDIA_UNAVAILABLE when WhatsApp has expired the file.

ParametersJSON Schema
NameRequiredDescriptionDefault
save_toNoAbsolute directory to save into (default: <data-dir>/media)
account_idNoRegistry account id (default, work, …). Omit to resolve from chat_id or message_id, or the default account.
message_idYesA message with has_media=true

TDQS

A4.6/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already provide readOnlyHint, idempotentHint, and non-destructive flags, but the description adds substantial behavioral detail: the file is pre-decrypted, inline return for small images, detailed result field semantics (filename vs original_filename, caption nullability), id resolution in raw form, account fallback behavior, and the MEDIA_UNAVAILABLE error. This goes well beyond the annotations and fully informs the agent of side effects and edge cases.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is long but each section delivers unique information: action, result fields, id handling, failure case. It is front-loaded with the primary action and then flows logically into details. No fluff; every sentence adds value. However, the length is near the upper bound of what an agent can parse quickly, so it loses a point for brevity, not for redundancy.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With no output schema, the description must fully explain the return value, and it does: all result fields, their types, nullability, and relationships. It also covers error conditions (MEDIA_UNAVAILABLE) and resolution nuances across accounts. For a 3-parameter tool with this complexity, the description is comprehensive and leaves no functional gaps.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The schema already covers all three parameters with descriptions (100% coverage), giving baseline 3. The description adds meaningful context: it explains the id resolution format for message_id, the behavior of account_id when omitted (walks all linked accounts), and the default for save_to. This enriches parameter understanding beyond the schema, so a 4 is warranted.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description opens with a specific verb and resource: 'Download the photo/video/audio/document attached to a message and save it to disk.' It immediately distinguishes this from sibling tools like send_media (sending) and transcribe_audio (processing audio). The mention of inline return for small images further clarifies behavior.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description clearly implies when to use the tool (when a message has media to download) but does not explicitly contrast it with alternatives or state exclusions. It gives prerequisites implicitly via the message_id requirement ('A message with has_media=true' in schema) and explains behavior when account_id is omitted, which helps the agent decide. However, no explicit 'use this instead of X' guidance exists, so not a 5.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

edit_messageEdit a WhatsApp message you sentA

Replace the text of a message the linked account sent. WhatsApp only allows this within 15 minutes of sending; after that send a correction instead.

ParametersJSON Schema
NameRequiredDescriptionDefault
textYesThe replacement text
account_idNoRegistry account id (default, work, …). Omit to resolve from chat_id or message_id, or the default account.
message_idYesA message the linked account sent

TDQS

A4.4/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare the mutation safety profile (readOnly=false, destructive=false, idempotent=false), so the description's key added value is the 15-minute time-limit constraint, which is a real behavioral trait the agent cannot infer from annotations. It stops short of describing edit side effects or the result payload, but the time window is the critical disclosure.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Two sentences, zero filler, with the core action stated first and the constraint immediately after. Every clause earns its place.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a simple three-parameter mutation with full schema coverage and annotations carrying the safety profile, the description supplies the one non-obvious constraint (the edit window). No output schema exists, so return values need not be explained; only minor gaps like side-effect detail remain.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so message_id, text, and account_id are already documented inline. The description confirms the target is a message 'the linked account sent' but adds no syntax or format detail beyond the schema. Baseline 3 applies.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb ('Replace the text') and resource ('a message the linked account sent'), which cleanly separates it from siblings like send_message and delete_message. An agent can identify the operation without opening the schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Gives an explicit precondition (only within 15 minutes of sending) and the alternative action when that window has passed ('send a correction instead'). Both when-to-use and when-not-to-use are covered.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

forward_messageDraft a forwarded WhatsApp messageA

Draft a forward of an existing message to another chat. Does not send. The recipient will see it marked as forwarded. Show the preview; after the user says yes, call confirm_send.

ParametersJSON Schema
NameRequiredDescriptionDefault
account_idNoRegistry account id (default, work, …). Omit to resolve from chat_id or message_id, or the default account.
message_idYesMessage id from read_messages / search_messages / get_message, e.g. "false_4072...@s.whatsapp.net_3EB0..."
to_chat_idYesDestination chat

TDQS

A4.2/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations cover the safety profile (readOnlyHint=false, destructiveHint=false, non-idempotent, openWorld). The description adds genuinely new behavioral context: nothing is transmitted until confirmed, the recipient sees the message marked as forwarded, and a preview is surfaced. That goes beyond what the structured fields convey.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Three short sentences with zero filler; the critical constraint ('Does not send') and the next-step routing are front-loaded and unambiguous.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a 3-parameter drafting tool with no output schema, the description covers the key concerns: no transmission occurs, the preview should be shown, and the confirmation path is named. It stops short of describing what the draft object returns, but nothing needed for correct invocation is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so message_id, to_chat_id and account_id are already fully documented inline. The description mentions 'existing message' and 'another chat' but adds no format, resolution fallback, or constraint detail beyond the schema, so the baseline of 3 applies.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

Specific verb ('Draft a forward') plus a specific resource ('an existing message to another chat'), with immediate disambiguation from sending tools via 'Does not send.' An agent can distinguish it from send_message/confirm_send without opening a schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description clearly scopes the operation ('Does not send') and routes the agent to the follow-up tool and its trigger ('after the user says yes, call confirm_send'). It doesn't explicitly compare against alternatives like send_message for non-forward cases, but the workflow condition is stated plainly.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

get_contactGet WhatsApp contact detailsA
Read-onlyIdempotent

Full details for one contact: name, number, about text, profile picture URL, whether they are a saved contact, a business, or blocked. name_source says where the shown name comes from — "contact" when it is the saved address-book name (is_my_contact), "pushname" when it is a name the person publishes, or "none" when there is no usable name.

ParametersJSON Schema
NameRequiredDescriptionDefault
account_idNoRegistry account id (default, work, …). Omit to resolve from chat_id or message_id, or the default account.
contact_idYesContact id from search_contacts / list_chats, or a phone number

TDQS

A4.2/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnlyHint=true, idempotentHint=true, and destructiveHint=false, so the safety profile is covered. The description adds value by detailing the return fields and explaining the name_source enum, which is beyond what annotations provide. It does not mention potential errors or rate limits, but for a read-only fetch that is not critical. It aligns with annotations and enhances understanding of the tool's behavior.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is two sentences: the first states the purpose and lists fields, the second explains name_source. It is front-loaded with the core purpose and contains no filler. Every sentence earns its place, and the structure is clean and scannable.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a simple read-only tool with two parameters and no output schema, the description is quite complete. It enumerates the returned fields and clarifies the ambiguous name_source values, which is essential for correct interpretation. It does not cover error handling or edge cases, but those are not typically required for such a straightforward getter. Overall, it provides enough context for an agent to use the tool effectively.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so both parameters (account_id and contact_id) are already well-documented. The description does not add new parameter semantics beyond the schema; it merely references the same sources for contact_id. Since the schema handles the heavy lifting, the description meets the baseline of 3 but does not exceed it.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states it retrieves full details for a single contact and lists the specific fields (name, number, about text, profile picture URL, saved/business/blocked status). This distinguishes it from sibling tools like get_message (message details) and get_group_info (group details), and from update_contact_details (which is a write operation). The verb 'get' plus the resource 'contact' is precise and unambiguous.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description implicitly indicates usage by mentioning 'one contact' and referencing contact_id sources (search_contacts/list_chats), which tells an agent where to obtain the identifier. However, it does not explicitly state when to prefer this over search_contacts (e.g., when you need a single contact's full profile versus a list) or when not to use it. It gives clear context but no exclusions, so it earns a 4.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

get_group_infoGet WhatsApp group infoA
Read-onlyIdempotent

Details of a group: name, description, owner, creation date, whether only admins may post, whether the linked account is an admin, and the participant list (up to 500; participant_count is always the true total). The invite link is included only when the linked account is an admin. The settings come too: info_locked (only admins edit the name, description and photo), member_add_mode ("admins" or "all"), join_approval, disappearing_seconds (0 when off), and community ({is_community, parent_group_id}) when the group is a community or belongs to one.

Call this before manage_group: most group actions need admin rights.

ParametersJSON Schema
NameRequiredDescriptionDefault
group_idYesGroup chat id ("<id>@g.us")
account_idNoRegistry account id (default, work, …). Omit to resolve from chat_id or message_id, or the default account.

TDQS

A4.5/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Beyond the readOnly/idempotent/destructive annotations, the description discloses important behavioral nuances: participant list is capped at 500 while participant_count is the true total, invite links are returned only for admins, and setting fields like info_locked and member_add_mode have specific meanings. This is substantial added context beyond the annotations.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is dense but well-structured, front-loading the core result fields and then adding the conditional invite-link behavior and settings. No sentence is wasted, and the manage_group guidance is placed at the end without bloating the definition.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the absence of an output schema, the description carries the burden of explaining the return shape, and it does so thoroughly: fields, limits, conditional values, and setting semantics are all covered. The parameter schema is complete, and the usage context is clear, making the tool fully callable by an agent.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

With 100% schema description coverage, the schema already documents both group_id and account_id thoroughly. The description adds only indirect context about 'the linked account' being an admin, which lightly reinforces the account_id parameter's role but does not materially extend the schema.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly identifies a retrieval tool: 'Details of a group' followed by a concrete list of returned attributes (name, description, owner, settings, participants). It also differentiates itself from the sibling manage_group by positioning itself as the prerequisite call.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description gives an explicit usage directive: 'Call this before manage_group: most group actions need admin rights.' This tells the agent when to use the tool, but it does not mention when not to use it or compare it against alternative info-gathering siblings.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

get_messageGet one WhatsApp message in fullA
Read-onlyIdempotent

The complete message behind a message_id, including the quoted message it replies to, each reaction with who left it, who chose each option of a poll or answered an event, and its media metadata. Use it after search_messages or read_messages when you need the context around a single message.

On the user's own messages, delivery.status says how far it got ("sent", "delivered", "read", "played", "pending" or "error"), and in a group read_by and delivered_to name who, with the time. It stops at "delivered" or is missing when read receipts are off on either side, and large groups may send none.

The id also resolves in its raw form: false_<lid>@lid_<stanza> works even when the chat's number was never learned, and an id that names the same message under the lid or the paired number finds it either way. With several accounts linked and no account_id, an id the resolved account cannot find is tried on each of the others in turn before MESSAGE_NOT_FOUND comes back, and the answer's account_id names the one that had it; pass account_id to keep the lookup on one account.

The sender carries the same identity fields as search_messages: id (the canonical jid — a …@lid only while WhatsApp has never revealed the paired number), phone (the number, or null for an unresolved lid), is_saved (whether the sender is in the user's address book), contact_name (the name saved there, or null), pushname (the name the sender publishes, when that is the name name shows) and name_source ("contact", "pushname" or "none" — which of those name came from).

ParametersJSON Schema
NameRequiredDescriptionDefault
account_idNoRegistry account id (default, work, …). Omit to resolve from chat_id or message_id, or the default account.
message_idYesMessage id from read_messages / search_messages / get_message, e.g. "false_4072...@s.whatsapp.net_3EB0..."

TDQS

A4.8/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnlyHint, openWorldHint, idempotentHint, and destructiveHint false, so the safety profile is clear. The description adds substantial behavioral detail: delivery status semantics, read-receipt behavior in groups, raw lid id resolution, cross-account fallback, and the exact shape of sender identity fields. This goes far beyond the annotations and gives an agent precise expectations.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is long but well-structured, with clear paragraphs for purpose, delivery status, id resolution, and sender fields. Each sentence carries substantive information. It is not wasteful, but it is longer than strictly necessary for a 2-parameter tool; however, given the complexity of WhatsApp messaging semantics, the length is justified. The core purpose is front-loaded, so it remains navigable.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a tool with no output schema, the description fully explains what to expect in the response: message content, quoted message, reactions, poll/event answers, media metadata, delivery status, and sender identity fields. It covers id resolution edge cases and account behavior. Nothing an agent needs to call it correctly and interpret results is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The schema already describes both parameters well (account_id and message_id with examples), so the baseline is 3. The description adds meaningful extra semantics: how message_id can be a raw lid form, how resolution falls back across accounts, and how account_id restricts the lookup. It does not fully cover every edge case, but it enriches the schema meaningfully, earning a 4.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description states a clear verb ('get') and resource ('message'), and specifies exactly what it returns: the complete message with quoted replies, reactions, poll/event answers, and media metadata. It explicitly differentiates itself from siblings like search_messages and read_messages by positioning itself as the follow-up for single-message context.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It gives explicit when-to-use guidance: 'Use it after search_messages or read_messages when you need the context around a single message.' This clearly tells an agent the appropriate trigger and distinguishes it from list-retrieval tools. It also covers account-resolution nuances, leaving no ambiguity about when to pass account_id.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

get_recent_messagesGet every WhatsApp conversation from the last N hoursA
Read-onlyIdempotent

Everything that happened recently, grouped by chat. This is the catch-up tool: one call instead of list_chats plus a read_messages per chat. WhatsApp's own notices — device linking, group membership changes, encryption notices — are left out so the counts are conversation; pass include_system to see them. A chat lists at most its newest 2,000 messages of the window.

ParametersJSON Schema
NameRequiredDescriptionDefault
hoursNoLook-back window in hours (1-168)
typesNoKeep only these message types; omit for every type. The limit counts matching messages, so ["call"] returns that many calls, not that many messages of which some are calls.
filterNoRestrict to unread chats, groups, or one-to-one chatsall
compactNoLeave out media without a caption and messages with no words in them, fold what one person sent in a row into one line, and say per chat what was left out. About half the size; use it for a routine catch-up
account_idNoRegistry account id (default, work, …). Omit to resolve from chat_id or message_id, or the default account.
include_systemNoInclude WhatsApp's own system notices, which are excluded from the bodies and the counts by default
include_previewsNoAttach a small JPEG of each photo, newest first, up to 12 per call, so you can see what was sent: the preview WhatsApp shipped when there is one, otherwise the photo is downloaded once and shrunk on the machine running wazap

TDQS

A4.7/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Beyond the readOnly/idempotent annotations, it discloses meaningful behavior: system notices are excluded from bodies and counts by default, include_system re-enables them, results are grouped by chat, and each chat is capped at its newest 2,000 messages. This is substantial behavioral context an agent would not otherwise know.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Four sentences with no filler; the core purpose is front-loaded, and every sentence adds either scope, usage context, or a behavioral limit. It is concise without sacrificing needed detail.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a 7-parameter, all-optional tool with rich schema descriptions and safety annotations, the description supplies the essential high-level semantics: grouping, system-notice handling, and the per-chat message cap. No critical selection or invocation information is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the schema already documents all seven parameters. The tool description adds little parameter meaning beyond mentioning include_system, which the schema already covers; baseline 3 applies.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description identifies a specific verb and resource: retrieving recent WhatsApp conversations grouped by chat, and positions it as 'the catch-up tool.' It distinguishes itself from the composed alternative of list_chats plus read_messages, making its purpose unmistakable.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It explicitly states when to use it: 'This is the catch-up tool: one call instead of list_chats plus a read_messages per chat.' This names the alternative and the efficiency condition, giving an agent clear selection guidance.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

get_statusGet the WhatsApp connection statusA
Read-onlyIdempotent

Check the session: connection status ("connected" means the tools work, "not_linked" means the user must run npx wazap-mcp login), whether the initial history sync has finished, which account is linked, how fresh the local history is — history shows when a message last arrived and flags stale when the phone has been quiet for a day while connected — the versions and data directory in use, and how many contacts carry a name from the phone's address book (contacts_named: 0 means it never arrived). webhook.delivery counts the account's webhook events: delivered (last 7 days), failed and cancelled (last 30), pending; a non-zero consecutive_failures or retrying means the receiver is refusing or unreachable right now, and last_failure says how.

Call this whenever another tool reports NOT_CONNECTED, NOT_LINKED or SYNC_IN_PROGRESS, or to confirm which account you are about to send from. Without account_id the top-level fields are the default account, plus accounts listing every live one. While a link is in progress the status is "linking" and pairing carries the code the user still has to type into their phone.

ParametersJSON Schema
NameRequiredDescriptionDefault
account_idNoRegistry account id (default, work, …). Omit to resolve from chat_id or message_id, or the default account.

TDQS

A4.7/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already mark this as read-only, idempotent, and non-destructive. The description goes far beyond that by explaining what each status means, what 'stale' indicates, what contacts_named: 0 implies, how webhook delivery counters behave, and what happens while a link is in progress. This is rich behavioral context with no contradiction.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is long but every clause carries diagnostic information that the agent needs, especially since there is no output schema. It front-loads the primary purpose and the trigger conditions, then details fields and failure semantics. The length is justified by the breadth of status data covered.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With no output schema, the description must compensate, and it does: it covers all major status categories, linking behavior, default-account handling, webhook delivery fields, and staleness semantics. An agent can correctly interpret the tool's response and decide when to invoke it without missing critical context.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

There is one optional parameter and the schema already covers its meaning at 100%. The description adds value by explaining the default-account behavior when account_id is omitted and that the response then also includes an accounts list. This goes beyond the schema's 'resolve from chat_id or message_id' phrasing.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description opens with 'Check the session' and then enumerates exactly what the tool reports: connection status, history sync, linked account, freshness, versions, data directory, contacts_named, and webhook delivery metrics. This clearly distinguishes it from siblings like send_message or get_recent_messages, which perform different actions.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description gives explicit trigger conditions: 'Call this whenever another tool reports NOT_CONNECTED, NOT_LINKED or SYNC_IN_PROGRESS, or to confirm which account you are about to send from.' It also explains account_id omission behavior. It lacks an explicit when-not-to-use or named alternative, but the context is clear and actionable.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

get_storiesSee the stories people postedA
Read-onlyIdempotent

The stories (status updates) the linked account has received in the last N hours, newest first, each with its author, its text or caption and its time. WhatsApp keeps a story for a day and so does wazap; nothing older is held. With include_previews the photos come as small images, and download_media works on a story's message_id like on any message. Stories never appear in chats, catch-ups or waits; this is the only place they show.

ParametersJSON Schema
NameRequiredDescriptionDefault
hoursNoLook-back window in hours (1-24)
account_idNoRegistry account id (default, work, …). Omit to resolve from chat_id or message_id, or the default account.
include_previewsNoAttach a small JPEG of each photo, newest first, up to 12 per call, so you can see what was sent: the preview WhatsApp shipped when there is one, otherwise the photo is downloaded once and shrunk on the machine running wazap

TDQS

A4.4/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already cover read-only/idempotent/open-world safety, and the description adds real substance on top: a 24-hour retention rule ('nothing older is held'), newest-first ordering, and how previews are produced (WhatsApp's preview when available, otherwise downloaded once and shrunk locally). It stops short of covering auth or per-call cost implications.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Four sentences, front-loaded with scope and ordering before mechanics; nearly every clause carries information. Slight redundancy in 'WhatsApp keeps a story for a day and so does wazap' costs a point but not much.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With no output schema, the description carries the return shape (author, text/caption, time, optional preview images) and the retention/ordering rules, which is everything an agent needs to invoke and interpret this read-only tool correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, which sets a baseline of 3, but the description adds meaning beyond the schema: previews arrive as 'small images', the retention ceiling explains why hours caps at 24, and it links message_id semantics to download_media. account_id receives no additional explanation, keeping it from a 5.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and resource: the stories (status updates) the linked account has received in the last N hours, newest first, each with author, text/caption and time. It explicitly distinguishes itself from siblings by noting stories never appear in chats, catch-ups or waits, so an agent can route correctly without opening a schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Gives a clear when-not for sibling tools ('Stories never appear in chats, catch-ups or waits; this is the only place they show') and points to download_media for retrieving a story's media via message_id. It lacks explicit guidance on choosing hours or when previews are worth requesting, but the routing context is strong.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

get_unansweredFind who is waiting on the userA
Read-onlyIdempotent

Chats where the last word is theirs and it asks for something: a question, a request ("poți", "te rog", "can you", "when"…), or a voice note nobody has heard yet. A conversation that ended in "ok, thanks" is not listed, and neither is an ask older than max_age_hours (two weeks by default): that one was abandoned, not left waiting. Groups count only when the user was @-mentioned or replied to after their own last message. A [business] account's ask is often an automatic reply; weigh it accordingly.

People come first, then the oldest wait. Each entry quotes the ask, says how many of their messages arrived since the user's last one, and how long they have been waiting. This is the follow-up half of an inbox triage; use get_recent_messages for what happened, and this for who is still waiting.

ParametersJSON Schema
NameRequiredDescriptionDefault
limitNoMaximum number of chats (1-50)
account_idNoRegistry account id (default, work, …). Omit to resolve from chat_id or message_id, or the default account.
max_age_hoursNoIgnore asks older than this; an ask left for two weeks (the default) is abandoned, not waiting
min_age_hoursNoOnly asks at least this old, e.g. 48 for people the user forgot for two days

TDQS

A4.8/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already cover safety (read-only, idempotent, non-destructive), and the description goes well beyond them: abandonment semantics for max_age_hours, group-chat inclusion logic, business-account auto-reply weighting, and output ordering. This is rich behavioral context an agent cannot infer from the structured fields.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loads the core definition, then layers exclusions, group logic, output shape, and routing. It is dense but a few parenthetical example tokens and the caveat sentences make it longer than strictly necessary; still, nearly every sentence carries selection-relevant information.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

No output schema exists, and the description still tells the agent what each entry contains (the quoted ask, count of messages since the user's last reply, waiting duration) plus ordering (people first, then oldest wait). Nothing material for correct invocation is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so the schema already documents all four parameters. The description still adds interpretive value: max_age_hours is framed as 'abandoned, not waiting,' and min_age_hours is illustrated with 48 hours for forgotten asks, giving an agent a reason to choose thresholds rather than just valid ranges.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific resource ('chats where the last word is theirs and it asks for something') with the exact inclusion heuristic, and explicitly distinguishes itself from the sibling get_recent_messages. An agent can tell what this returns and why it exists without opening the schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicit routing: 'use get_recent_messages for what happened, and this for who is still waiting.' It also gives exclusion conditions (ended in 'ok, thanks', older than max_age_hours, group chats without @-mention or reply) and caveats for business accounts, so the agent knows when not to expect a row.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

join_groupJoin a WhatsApp group from an inviteA

Join a group from an invite: a link (https://chat.whatsapp.com/, or the code alone) as invite, or the message_id of an invite message someone sent (type "invite"). Exactly one of invite / message_id.

Without confirm: true it joins nothing and returns the group's name, description, member count and whether an admin must approve new members. Show that to the user and wait for an explicit yes, then call again with the same invite or message_id and confirm: true. Every member sees the account join. The answer is the group's chat_id, or pending_approval when an admin must let the account in first.

ParametersJSON Schema
NameRequiredDescriptionDefault
inviteNoA https://chat.whatsapp.com/ link, or its code
confirmNotrue joins, only after the user said yes to the preview; omit to preview
account_idNoRegistry account id (default, work, …). Omit to resolve from chat_id or message_id, or the default account.
message_idNoAn invite message (type "invite") from read_messages

TDQS

A4.7/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

The description goes well beyond the annotations by explaining the preview behavior ("Without confirm: true it joins nothing"), the visible side effect ("Every member sees the account join"), and the pending_approval result. This is particularly valuable because idempotentHint: false and readOnlyHint: false leave behavioral details unexplained without extra context.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is detailed but every sentence earns its place: the invite formats, the confirm condition, the return values, and the visible side effect are all stated without redundancy or filler. The structure is front-loaded with the core mechanics before the confirmation instructions.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a 4-parameter two-phase tool with no output schema, the description is complete: it defines the inputs, the preview response fields, the confirmed response (chat_id or pending_approval), and the side effect of joining. An agent has enough context to call the tool correctly and interpret its result.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so the baseline is 3, but the description adds meaningful parameter semantics beyond the schema: it clarifies the exact-one-of requirement for invite/message_id and distinguishes the preview call from the confirm call. This extra guidance justifies a 4.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the action as "Join a group from an invite" and specifies the accepted invite forms (link or code, or invite message_id), which distinguishes it from sibling tools like create_group, get_group_info, and manage_group. The title and description together convey the exact scope of the operation without ambiguity.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It gives explicit invocation guidance: exactly one of invite / message_id must be supplied, and the two-phase confirm flow is described step by step (preview without confirm: true, then call again with confirm: true after user consent). It does not explicitly name alternative tools or exclusions, but provides clear context for when this tool should be used.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

learnLearn how to use the WhatsApp toolsA
Read-onlyIdempotent

Read this FIRST, before any other WhatsApp tool. Returns the guide to the tools, the id formats, the recommended workflows, the message shape and every error code with what to do about it. Takes no arguments and never touches WhatsApp.

ParametersJSON Schema
NameRequiredDescriptionDefault
account_idNoRegistry account id (default, work, …). Omit to resolve from chat_id or message_id, or the default account.

TDQS

A4.5/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnly/idempotent/non-destructive/closed-world, so the safety bar is low. The description adds value by specifying exactly what content is returned (guide, id formats, workflows, message shape, error remediation) and reinforcing that it 'never touches WhatsApp,' useful given no output schema. Minor tension: it says 'Takes no arguments' while the schema exposes an optional account_id.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Three tight sentences with the imperative 'Read this FIRST' front-loaded, then the return contents, then the no-side-effect guarantee. No wasted words for a documentation tool.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a zero-required-param meta tool with no output schema, the description fully covers purpose, timing, return contents, and side-effect profile. Nothing an agent needs before calling it is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so account_id's meaning is already fully documented in the schema, which sets the baseline at 3. The description's claim that it 'takes no arguments' adds no detail and is slightly at odds with the optional account_id, so it does not earn above baseline.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and resource: returns the guide covering tools, id formats, recommended workflows, message shape, and error codes. The title and description together make clear this is the meta/documentation entry point, cleanly distinguished from all 32 sibling action tools.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicitly instructs 'Read this FIRST, before any other WhatsApp tool,' which is an unambiguous when-to-use directive. For a bootstrap/documentation tool there is no meaningful alternative to exclude, so the guidance is complete.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

list_accountsList WhatsApp accounts on this serverA
Read-onlyIdempotent

List every configured WhatsApp account: id, name, connection status, masked phone, owner name, and whether that account allows writes. Call this first when more than one account is linked, then pass account_id on the other tools. Takes no arguments besides the optional account_id (ignored for the listing).

ParametersJSON Schema
NameRequiredDescriptionDefault
account_idNoRegistry account id (default, work, …). Omit to resolve from chat_id or message_id, or the default account.

TDQS

A4.3/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnly, idempotent, non-destructive and openWorld, so the safety profile is covered. With no output schema, the description adds real behavioral value by naming the returned fields, including that phones are masked and that write permission is exposed, plus the note that account_id is effectively ignored for listing.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Three tight sentences with the resource and field list front-loaded, followed by routing guidance and the argument note. Nothing is wasted, though the clause about account_id being ignored is slightly at odds with the schema's resolution wording and could be sharper.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a zero-required-argument read tool with no output schema, the definition covers what is returned, when to call it, and how to chain account_id into other tools. Pagination or account-limit behavior is not mentioned, but for an account registry that is a minor gap.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100% so the schema already documents account_id (default, work, resolution from chat_id/message_id). The description adds a nuance the schema does not state plainly: that account_id is ignored for this listing call, clarifying that it does not filter results.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and resource ('List every configured WhatsApp account') and enumerates the returned fields (id, name, connection status, masked phone, owner, write permission), so an agent knows exactly what the tool produces. No sibling tool overlaps this listing behavior, so differentiation is implicit but clear.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Gives explicit sequencing guidance: 'Call this first when more than one account is linked, then pass account_id on the other tools.' That is clear context for when to use it, though it offers no exclusions or naming of an alternative listing path (e.g., resolving accounts implicitly elsewhere).

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

list_chatsList WhatsApp chatsA
Read-onlyIdempotent

List conversations, most recently active first. Use it to discover the chat_id values the other tools need.

Each chat has: chat_id, name, type, unread_count, last_message {text, timestamp, from_me}, archived, pinned, muted_until, and left (groups you are no longer in).

ParametersJSON Schema
NameRequiredDescriptionDefault
limitNoMaximum number of chats (1-100)
filterNoWhich chats to list; "all" (default) excludes archived onesall
account_idNoRegistry account id (default, work, …). Omit to resolve from chat_id or message_id, or the default account.

TDQS

A4.2/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnly/idempotent/destructive=false, so the safety profile is covered. The description adds genuinely non-derivable behavior: result ordering, and the semantics of fields like 'left' (groups you are no longer in) — useful since there is no output schema to document them.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Two tight sentences, front-loaded with purpose and ordering before the return-shape list. Every clause carries information an agent needs; nothing is padded.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With no output schema, enumerating the returned fields is the right compensating move, and the ordering plus 'left' semantics round it out. Only minor gaps remain, such as pagination guidance beyond the limit cap — which the schema already bounds.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, including the subtle note that filter="all" excludes archived chats, so the schema already carries the parameter meaning. The description adds no param syntax, defaults, or interaction guidance (e.g. how limit interacts with filter), so baseline 3 applies.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and resource ('List conversations') plus the ordering guarantee ('most recently active first'), and frames the tool's role as a chat_id discovery step. This clearly separates it from siblings like read_messages or search_messages, which consume those ids rather than produce them.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

'Use it to discover the chat_id values the other tools need' gives a concrete condition for choosing this tool over its siblings. It stops short of naming specific alternatives or exclusions (e.g. when to prefer search_contacts for a known name), but the routing intent is unambiguous.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

manage_chatManage a WhatsApp chatA
Destructive

Change a chat, or a message in it. Actions:

  • archive / unarchive, pin / unpin (the chat), mute / unmute (mute_hours defaults to 8), mark_read (sends read receipts) / mark_unread

  • pin_message / unpin_message — need message_id; pins it for everyone in the chat, for pin_hours 24, 168 (default) or 720

  • star_message / unstar_message — need message_id; the star is the linked account's own

  • clear — DESTRUCTIVE, empties the chat for the linked account only

  • delete — DESTRUCTIVE, deletes the chat for the linked account only

  • block / unblock — a person's chat only; a blocked person can no longer message or call the account

A message_id must belong to chat_id. Every action hits WhatsApp at once and there is no draft: say what will change and wait for a yes before calling it.

ParametersJSON Schema
NameRequiredDescriptionDefault
actionYesWhat to do with the chat
chat_idYesChat id as returned by another tool ("<digits>@s.whatsapp.net" or "<id>@g.us"), or a phone number in international format
pin_hoursNoHow long pin_message keeps the message pinned: 24, 168 (default) or 720 hours
account_idNoRegistry account id (default, work, …). Omit to resolve from chat_id or message_id, or the default account.
message_idNoThe message for pin_message, unpin_message, star_message and unstar_message; it must be in chat_id
mute_hoursNoHours to mute, default 8; only used by "mute"

TDQS

A4.3/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

The annotations already declare destructiveHint=true and readOnlyHint=false; the description adds critical behavioral context beyond those: which actions are DESTRUCTIVE, that clear/delete affect only the linked account, that mark_read sends read receipts, and that every action hits WhatsApp at once with no draft. This materially improves an agent's ability to call the tool safely. It does not contradict the annotations.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is long, but its bullet-style list is scannable and information-dense. Each line justifies its place by explaining an action, a parameter requirement, a default, or a safety warning. The final warning about immediate execution and the need for confirmation is valuable and appropriately placed. Minor deduction for verbosity, but the complexity of the tool justifies it.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a tool with 16 actions, 6 parameters, destructive annotations, and no output schema, the description is substantially complete. It covers what each action does, necessary parameters, defaults, the person-chat restriction for block, and the no-draft/immediate-execution behavior. It does not specify response/return values, but with no output schema defined, that burden is reduced. Some per-action edge cases (e.g., preconditions for unpinning or unstarring) are not covered, which prevents a perfect score.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

With schema coverage at 100%, the baseline is 3. The description goes beyond the schema by tying message_id specifically to pin/star actions and stating it must belong to chat_id, explaining mute_hours default, clarifying pin_hours allowed values and default, and spelling out the semantics of clear vs delete. This adds meaning beyond the structured schema.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description states a specific verb-resource pair ('Change a chat, or a message in it') and enumerates 16 distinct actions. It clearly distinguishes this tool from siblings like send_message, delete_message, and edit_message by covering the full set of chat- and message-management actions.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description gives clear when-to-use context for each action: which need message_id, that block/unblock only applies to person chats, and that clear/delete are destructive only-for-linked-account operations. It does not explicitly name sibling tools as alternatives, but the exhaustive action list and the destructive/confirm warning make operation selection unambiguous. Minor deduction for not explicitly stating when not to use this tool relative to sibling tools.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

manage_groupManage a WhatsApp groupA
Destructive

Administer a group. Actions:

  • add / remove / promote / demote — need participant_ids; each participant comes back with status ok, invite_needed or failed

  • leave — DESTRUCTIVE, rejoining needs an invite

  • set_subject / set_description — need value

  • set_picture — needs exactly one of file_path / url: JPEG, PNG or WebP, at most 10 MB. Every member sees it at once and there is no draft: show the image and wait for a yes first

  • remove_picture — takes the group photo down; ask first the same way

  • get_invite_link / revoke_invite_link

  • list_join_requests — who is waiting for approval, with when and how they asked

  • approve_join_requests / reject_join_requests — need participant_ids from list_join_requests; each comes back with status ok or failed

  • set_announcement_only / set_info_locked / set_join_approval — value "on" or "off"

  • set_add_mode — value "admins" or "all"

  • set_disappearing — value "off", "24h", "7d" or "90d"

Everything except leave requires the linked account to be a group admin; call get_group_info first to check. Every change is visible to all members at once: say what will change and wait for a yes before calling it.

ParametersJSON Schema
NameRequiredDescriptionDefault
urlNoset_picture: public http(s) URL to fetch and use as the photo
valueNoNew subject or description; "on"/"off" for set_announcement_only, set_info_locked, set_join_approval; "admins"/"all" for set_add_mode; "off"/"24h"/"7d"/"90d" for set_disappearing
actionYesGroup action to perform
group_idYesGroup chat id ("<id>@g.us")
file_pathNoset_picture: absolute path of a local JPEG, PNG or WebP
account_idNoRegistry account id (default, work, …). Omit to resolve from chat_id or message_id, or the default account.
participant_idsNoTargets of add/remove/promote/demote/approve_join_requests/reject_join_requests

TDQS

A4.8/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Goes well beyond the annotations by disclosing that leave is destructive and requires a new invite to rejoin, that all changes are immediately visible with no draft, and that set_picture requires explicit confirmation before sending. This adds genuinely useful behavioral context beyond destructiveHint, readOnlyHint, and idempotentHint.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is long because the tool supports 19 actions, but every sentence earns its place. It is front-loaded with the purpose, then organized as a scannable action list, followed by shared prerequisites and warnings. No filler or repetition.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a complex multi-action tool with no output schema, the description covers the critical context: admin requirements, destructive behavior, confirmation protocol, accepted values, and result statuses. An agent has enough information to invoke the correct action, supply the right parameters, and set user expectations.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Although schema coverage is 100%, the description adds important constraints not in the schema: exactly one of file_path or url for set_picture, image format/size limits, participant_ids for approve/reject must come from list_join_requests, and the status values returned per participant. This materially improves an agent's ability to construct valid calls.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States the verb 'Administer a group' and immediately enumerates the full set of group management actions (membership, subject, picture, join requests, settings). It clearly distinguishes itself from siblings like create_group, join_group, and get_group_info by covering post-creation administration.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Provides clear operational context: call get_group_info first to verify admin status, confirm with the user before making visible changes, and note that leave is destructive. It does not explicitly name alternatives such as manage_chat or create_group, but the action list makes the tool's scope unmistakable.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

mark_handledTake a chat off the waiting listA
Idempotent

The user dealt with what this chat was asking, outside WhatsApp or by a reply wazap did not see: a phone call, a meeting, a decision. The open ask is remembered as handled and the chat leaves get_unanswered. The next message from the other side makes a new ask and the chat comes back on its own. Kept on this machine only; nothing is sent or marked read on WhatsApp.

ParametersJSON Schema
NameRequiredDescriptionDefault
chat_idYesChat id as returned by another tool ("<digits>@s.whatsapp.net" or "<id>@g.us"), or a phone number in international format
account_idNoRegistry account id (default, work, …). Omit to resolve from chat_id or message_id, or the default account.

TDQS

A4.4/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Adds substantial behavior beyond the annotations: the mark is local-only, nothing is sent or marked read on WhatsApp, and the handled state auto-reverses when the other side next messages. This mirrors and enriches the idempotentHint=true / destructiveHint=false annotations rather than contradicting them.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Three sentences, front-loaded with the triggering situation before the state effect and the local-only caveat. Dense but each sentence carries distinct information; the WhatsApp reply aside is slightly incidental but still clarifying.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With annotations covering safety/idempotency and a fully described schema, the description supplies the remaining behavioral context an agent needs: what is mutated, what is not transmitted to WhatsApp, and how the state reverses on its own.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so both chat_id and account_id are already documented in the schema. The description adds no parameter-level syntax, format, or resolution detail beyond that, which is the expected baseline when the schema does the work.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb (mark handled) on a specific resource (the open ask in a chat) and names the observable effect: the chat leaves get_unanswered. This lets an agent distinguish it from siblings like get_unanswered or manage_chat without opening any schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Gives a clear triggering condition: the user dealt with the ask outside WhatsApp (phone call, meeting, decision). It also contrasts the state transition with get_unanswered's listing behavior, though it does not explicitly name an alternative tool or an exclusion case.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

react_to_messageReact to a WhatsApp messageA

Add an emoji reaction to a message, or pass an empty string to remove your reaction.

ParametersJSON Schema
NameRequiredDescriptionDefault
emojiYesA single emoji such as "👍", or "" to remove your reaction
account_idNoRegistry account id (default, work, …). Omit to resolve from chat_id or message_id, or the default account.
message_idYesMessage id from read_messages / search_messages / get_message, e.g. "false_4072...@s.whatsapp.net_3EB0..."

TDQS

A3.6/5.0
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations cover the safety profile (readOnlyHint=false, destructiveHint=false, openWorldHint=true), so the agent knows this is a non-destructive mutation. The description's one notable behavioral addition, that an empty string removes the reaction, is already duplicated in the schema parameter description. It adds no auth, rate-limit, or return-value context beyond what structured fields supply.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

A single well-formed sentence that front-loads the primary action and includes the removal case with zero padding. Nothing is wasted.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a simple 3-parameter mutation with no output schema, the description plus the 100%-covered schema convey everything needed to call it correctly. Only minor gaps remain (no mention of account context or response behavior on success/failure), which are low-stakes here.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so all three parameters (emoji, account_id, message_id) are fully documented in the schema, including the empty-string removal trick and account resolution fallback. The description adds no syntax or semantic detail beyond that, so the baseline of 3 is appropriate.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description states a specific verb+resource ('add an emoji reaction to a message') and includes the inverse operation (removal), making the tool's scope unambiguous. It does not explicitly name a sibling tool, but the reaction semantics are specific enough to distinguish it from send_message or edit_message.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Usage is implied by the description ('add an emoji reaction' / pass empty string to remove), which effectively tells an agent how to invoke both modes. However, there is no explicit guidance about when to use this versus alternatives like send_message, nor any prerequisites or account resolution notes.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

read_messagesRead messages from a WhatsApp chatA
Read-onlyIdempotent

Read messages from one chat, oldest to newest.

Without before you get the most recent messages. Pass before (the oldest message_id you already have) to page further back; wazap asks the phone for older history when the local store runs out, which takes a few seconds.

ParametersJSON Schema
NameRequiredDescriptionDefault
limitNoMaximum number of messages (1-200)
typesNoKeep only these message types; omit for every type. The limit counts matching messages, so ["call"] returns that many calls, not that many messages of which some are calls.
beforeNoReturn the messages immediately older than this message_id
chat_idYesChat id as returned by another tool ("<digits>@s.whatsapp.net" or "<id>@g.us"), or a phone number in international format
account_idNoRegistry account id (default, work, …). Omit to resolve from chat_id or message_id, or the default account.
include_previewsNoAttach a small JPEG of each photo, newest first, up to 12 per call, so you can see what was sent: the preview WhatsApp shipped when there is one, otherwise the photo is downloaded once and shrunk on the machine running wazap

TDQS

A4.2/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already cover readOnlyHint=true, idempotentHint=true, and destructiveHint=false, so the safety profile is established. The description adds genuine behavioral value beyond those: the phone-side fetch on cache exhaustion and its latency ('takes a few seconds'), which an agent needs to anticipate slow calls.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Two short paragraphs with zero waste. The core purpose and ordering are front-loaded in the first sentence, followed only by the essential paging/latency behavior. Every sentence earns its place.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

The description covers ordering, paging mechanics, and latency—the key non-obvious behaviors. Given the schema handles all parameter semantics and annotations handle the safety profile, nothing critical is missing for an agent to call this tool correctly. A return-format description would be a minor enhancement but is not required.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the schema fully documents all six parameters. The description body adds some practical context on how `before` drives paging, but since the schema already describes each parameter, the description contributes only marginal extra meaning, matching the baseline 3.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb+resource ('Read messages from one chat') with explicit ordering ('oldest to newest'). The paging detail distinguishes it from siblings like get_recent_messages (recent only) and get_message (single message), making the scope unambiguous.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Provides explicit, actionable paging guidance: 'Without `before` you get the most recent messages. Pass `before` ... to page further back'. This clearly instructs when and how to use the paging parameter, though it does not explicitly name alternatives or state when-not-to-use cases against sibling tools.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

recallSemantically search WhatsApp historyA
Read-onlyIdempotent

Search by meaning and by words at once over the whole WhatsApp history — answers "the invoice Dan mentioned", "the address Ana sent", "what did they say about the trip". A paraphrase or another language still hits through its meaning, and a short or foreign-language question still hits through its words; the two rankings are fused. A match found by meaning weighs a little less with age (85% a month on, never under 70%), so the fresher of two close matches comes first but a clearly closer old one still does; one chat takes at most three leading places before other chats' hits, and a near-duplicate trails the list. For an exact string — an id, a phone number, a URL — search_messages is the better tool.

Each result carries its date, a fused score, matched ("words", "meaning" or "both") and the cosine similarity when its meaning ranked it. chat_id, since, until and from narrow the search exactly like search_messages — including a name that resolves to exactly one person. A hit marked "index only" is a message wazap holds only as text: quote its words; get_message returns them, but download_media has nothing to open and it cannot be replied to or forwarded. A hit found only by meaning must clear the similarity floor, so a question with no answer comes back empty; when only weak meaning matches survive, the output says so — do not present them as found facts.

When semantic recall is off or its embedding setup is missing, the tool does not dead-end: it falls back to a keyword search over the local history, marked mode: "keyword_fallback", and recall_unavailable.fix names the command that turns semantic recall on. An error remains only when even the fallback cannot run.

Each hit's sender carries the same identity fields as search_messages, and freshness says whether the local history may be partial or stale. Every answer also declares its window: the semantic path reports how many messages the index covers, and a keyword fallback carries the same coverage block search_messages does.

ParametersJSON Schema
NameRequiredDescriptionDefault
fromNoOnly messages this person sent: "me", a phone number, a contact/chat id, or a name that resolves to exactly one person (the error names the candidates when it does not)
limitNoMaximum number of results (1-50)
queryYesWhat to find, said any way — the meaning is what matches
sinceNoOnly messages from this moment on: a date ("2026-09-01") or an ISO timestamp
untilNoOnly messages up to this moment: a date or an ISO timestamp
chat_idNoRestrict the search to this chat
account_idNoRegistry account id (default, work, …). Omit to resolve from chat_id or message_id, or the default account.

TDQS

A4.9/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Beyond the annotations (readOnly/openWorld/idempotent/non-destructive), the description discloses many behavioral traits: fused ranking, age weighting (85% a month, never under 70%), per-chat placement caps, near-duplicate handling, similarity floor, keyword fallback with mode marker, index-only limitations, and coverage/freshness reporting. This is far more than the annotations alone convey.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is long, but every paragraph earns its place given the tool's complex fusion and fallback behavior. It is front-loaded with the core purpose, then proceeds logically through output fields, parameter semantics, edge cases, and failure modes. There is no filler or tautology.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a sophisticated search tool with no output schema, the description covers return values, ranking semantics, filtering behavior, index-only limitations, fallback mode, error conditions, and coverage/freshness fields. Combined with 100% schema coverage and strong annotations, nothing essential is missing for correct invocation.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so the baseline is 3, but the description adds meaningful semantics: query is 'said any way — the meaning is what matches', from supports name resolution to exactly one person, and since/until narrow exactly like search_messages. It does not redundantly re-explain every field, yet enriches the params with behavioral detail.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description opens with a specific, vivid statement of what the tool does: 'Search by meaning and by words at once over the whole WhatsApp history'. It gives concrete example queries and explicitly contrasts itself with search_messages for exact strings, so an agent can distinguish it from siblings without inspecting schemas.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It explicitly names when to prefer search_messages ('For an exact string — an id, a phone number, a URL') and when this tool is the right choice (meaning-based or word-plus-meaning search). It also explains fallback behavior when semantic recall is unavailable, and says quote words for 'index only' hits with get_message as the recovery path.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

remove_contactRemove a WhatsApp contactA
Destructive

Take a person out of the account's WhatsApp contacts: the saved entry and its name go, the chat and its history stay. Nothing is sent to the contact.

ParametersJSON Schema
NameRequiredDescriptionDefault
account_idNoRegistry account id (default, work, …). Omit to resolve from chat_id or message_id, or the default account.
contact_idYesContact id from search_contacts / get_contact, or a phone number

TDQS

A4.2/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already indicate destructive and non-readonly behavior. The description adds specific consequences: the chat and history remain, and nothing is sent to the contact. This goes beyond annotations by clarifying what is NOT affected, which is valuable for an agent.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is two sentences with no filler. It front-loads the main action and then adds precise behavioral details. Every sentence earns its place.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a simple destructive tool, the description covers the key behavioral outcomes (what is removed, what persists, no notification). It does not mention return values or error cases, but given the schema coverage and annotations, it is adequately complete for an agent to invoke correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The input schema has 100% parameter description coverage, providing clear meanings for account_id and contact_id. The description adds no additional parameter-level information, so a baseline of 3 is appropriate.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description uses a specific verb ('remove') and resource ('contact'), and explicitly clarifies the scope: only the saved entry and name are removed, while chat history is preserved. This clearly distinguishes it from sibling tools like update_contact_details and save_contact, and from chat deletion tools.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description clearly states the operation (removing a contact) and provides contextual detail (chat history stays, no notification). However, it does not explicitly mention alternatives or when not to use this tool, though the intent is unambiguous from the context.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

save_contactAdd or rename a WhatsApp contactA

Save a person in the account's WhatsApp contacts: a new entry for a phone number, or a new name for an existing one. The name syncs to every linked device, and with save_on_phone (default) also into the phone's own address book. WhatsApp keeps no other fields — email, "my accountant" and the like go to set_contact_note, which stays on this machine.

ParametersJSON Schema
NameRequiredDescriptionDefault
nameYesFull name to save the contact under
account_idNoRegistry account id (default, work, …). Omit to resolve from chat_id or message_id, or the default account.
contact_idYesContact id from search_contacts / get_contact, or a phone number
first_nameNoFirst name, when it differs from the full name
save_on_phoneNoAlso write the contact into the phone's address book; false keeps it inside WhatsApp

TDQS

A4.4/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations indicate a non-readonly write operation, and the description adds concrete side effects: name syncs to all linked devices and optionally to the phone's address book. It also clarifies that no other fields are stored, which helps set expectations beyond what annotations convey.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is compact and front-loaded with the core purpose, then adds essential behavioral detail and a sibling distinction. Every sentence carries information without redundancy.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a write tool with full schema coverage and no output schema, the description covers the primary use cases, side effects, and scope. It lacks explicit error or prerequisite information, but the tool is straightforward and the provided detail is sufficient for correct invocation.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so the baseline is 3. The description adds meaning by explaining that save_on_phone controls phone-book writes and by clarifying that the name field is for the contact name only (with notes going elsewhere). This enriches the parameter semantics beyond the schema.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description states a clear verb (save) and resource (WhatsApp contacts), and distinguishes two use cases: creating a new entry or renaming an existing one. It also explicitly routes non-name fields to set_contact_note, which differentiates it from that sibling.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It clearly explains when to use the tool (new contact or rename) and gives an explicit exclusion for notes via set_contact_note. It doesn't enumerate all sibling tools but provides sufficient context to avoid the most common confusion.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

search_contactsSearch WhatsApp contactsA
Read-onlyIdempotent

Find contacts by name, phone number, tag or detail: a substring match on the name, a digit match on the number, or a word from a local tag or detail — so "contabil" finds the person filed under role: contabil by update_contact_details. With only tag it lists everyone carrying that tag. Returns contact_id values usable as chat_id.

ParametersJSON Schema
NameRequiredDescriptionDefault
tagNoOnly contacts filed under this tag ("client"); "#" optional
limitNoMaximum number of results (1-50)
queryNoName fragment, phone number, or tag/detail text (at least 2 characters). Omit with tag to list everyone carrying it.
account_idNoRegistry account id (default, work, …). Omit to resolve from chat_id or message_id, or the default account.

TDQS

A4.5/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Beyond the readOnly/openWorld/idempotent annotations, the description reveals matching semantics ('substring match on the name, a digit match on the number, or a word from a local tag or detail'), tag-only behavior, and that results are contact_id values usable as chat_id. No contradiction with annotations.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The core purpose is front-loaded in the first sentence, and the subsequent sentences each add non-obvious behavior (tag-only listing, return format). It is slightly longer than strictly necessary because of the worked example, but the example earns its place.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a read-only search tool with 100% schema coverage and no output schema, it explains matching modes, the tag-only case, and return value. The only noticeable gap is that behavior when combining both tag and query is not explicitly clarified.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema covers all parameters, so baseline is 3. The description adds meaningful semantics for query and tag: substring vs digit vs word matching, tag-only listing, and an example tying 'contabil' to a detail updated by update_contact_details. It does not add much for limit or account_id, but the schema already documents those.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description opens with 'Find contacts by name, phone number, tag or detail' and specifies the matching modes (substring, digit, word). It distinguishes itself from siblings like search_messages and get_contact by targeting contacts and returning contact_id usable as chat_id.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It clearly describes when to use the tool: any contact lookup by name, number, tag, or detail, and notes the tag-only case for listing all carriers. It does not explicitly name alternatives or exclusions, so it misses the top bar for explicit routing.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

search_messagesSearch WhatsApp messagesA
Read-onlyIdempotent

Substring search over the messages wazap holds locally — all chats, or one chat, including history synced on first link. Case and diacritics are ignored ("sedinta" finds "ședință"). Every message the phone synced to this device is kept and searched; there is no per-chat window. It cannot reach messages the phone never synced to this device.

Every answer declares the window it searched: coverage.searched counts the held messages in scope (the chat scope and time filters applied) and coverage.oldest_at/newest_at bound that window, so "no messages found" always says how much history was searched. coverage.per_chat_cap is null: no chat is capped. A query so short or so common that the search reaches its scan limit answers scan_capped: true and searched_back_to: messages older than that were not searched, so narrow it (chat_id, since/until, a longer query) before concluding nothing exists.

from accepts "me", a phone number, a contact/chat id, or a name: a name must resolve to exactly one person — it matches contact names, notify names and last-seen pushnames, then one-to-one chat display names — or the error lists the candidates it found.

Every message's sender carries id (the canonical jid — a …@lid only while WhatsApp has never revealed the paired number), phone (the number, or null for an unresolved lid), is_saved (the sender is in the user's address book — when false, treat the shown name as claimed, not known), contact_name (the name saved there, or null), pushname (the name the sender publishes, when that is the name name shows; null for saved contacts and unnamed senders) and name_source ("contact", "pushname" or "none" — which of those name came from). A sender wazap knows nothing about reads "unknown (lid …1234)" — never bare lid digits, which look like a phone number and are not one.

freshness says whether the history this searched may be partial (sync still running) or stale (nothing inbound for 24h while connected); on a scoped search freshness.chat is the newest message wazap holds for that chat.

ParametersJSON Schema
NameRequiredDescriptionDefault
fromNoOnly messages this person sent: "me", a phone number, a contact/chat id, or a name that resolves to exactly one person (the error names the candidates when it does not)
limitNoMaximum number of results (1-50)
queryYesText to search for
sinceNoOnly messages from this moment on: a date ("2026-09-01") or an ISO timestamp
untilNoOnly messages up to this moment: a date or an ISO timestamp
chat_idNoRestrict the search to this chat
account_idNoRegistry account id (default, work, …). Omit to resolve from chat_id or message_id, or the default account.

TDQS

A4.6/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Beyond the readOnly/idempotent/open-world annotations, the description discloses scan limits (scan_capped, searched_back_to), coverage semantics, sender-field meanings, and freshness staleness/partiality. This is extensive behavioral transparency with no contradiction against the annotations.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is long but dense; each paragraph covers a distinct aspect (scope, coverage/scans, from resolution, sender fields, freshness). Some repetition of the schema's `from` detail occurs, but given the complexity and lack of an output schema, the length is largely justified.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With no output schema, the description fully explains the meaningful return fields (`coverage`, `sender`, `freshness`) and the operational caveats (cap, sync, unsynced data). An agent has everything it needs to invoke the tool and interpret results correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The input schema already covers every parameter well (100% coverage), so baseline is 3. The description adds meaningful resolution semantics for `from` (contact names, notify names, pushnames, display names) and clarifies error behavior with candidates, plus coverage-related output context that helps interpret parameter effects.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description opens with a specific verb+resource ('Substring search over the messages wazap holds locally') and immediately defines scope (all chats or one chat) and key behavior (case/diacritics ignored). It clearly conveys what the tool does and distinguishes it from siblings like get_recent_messages or search_contacts without ambiguity.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It states the intended context (searching locally held WhatsApp messages) and gives actionable guidance for handling scan caps ('narrow it (chat_id, since/until, a longer query)') and explains the limit of unsynced messages. It does not explicitly point to alternative tools, but the usage context is clear enough.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

send_locationDraft a WhatsApp locationA

Draft a map pin, optionally labelled with a place name and address. Does not send. Show the preview; after the user says yes, call confirm_send.

ParametersJSON Schema
NameRequiredDescriptionDefault
nameNoPlace name shown on the pin
addressNoStreet address shown under the name
chat_idYesChat id as returned by another tool ("<digits>@s.whatsapp.net" or "<id>@g.us"), or a phone number in international format
latitudeYesLatitude in decimal degrees
longitudeYesLongitude in decimal degrees
account_idNoRegistry account id (default, work, …). Omit to resolve from chat_id or message_id, or the default account.

TDQS

A4.5/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations declare readOnlyHint=false, destructiveHint=false, idempotentHint=false, and openWorldHint=true, so the safety profile is partially covered. The description adds the non-obvious behavioral fact that this call produces a draft only and never transmits, which is meaningful beyond the annotations. It does not mention auth/account requirements, but the schema's account_id description covers that.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Three short sentences, front-loaded with the core action, followed by the critical 'does not send' caveat and the required next step. No filler.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a mutation tool with no output schema, the description supplies the workflow context an agent needs: it drafts rather than sends, a preview is shown, and confirm_send completes the action. Nothing essential to correct invocation is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the schema already documents every parameter (name, address, chat_id, latitude, longitude, account_id). The description only restates that name/address are optional labels, adding little semantics beyond the structured fields. Baseline 3 applies.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb ('Draft') and resource ('a map pin, optionally labelled with a place name and address'), and explicitly says 'Does not send', which separates it from the send-family siblings (send_media, send_message, confirm_send). An agent can identify the tool without opening the schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Gives an explicit operating procedure: show the preview, wait for user confirmation, then call confirm_send. This names the required follow-up sibling and the condition that triggers it, leaving nothing to inference.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

send_mediaDraft a WhatsApp media messageA

Draft an image, video, audio file, document or GIF, from a local path on the machine running wazap or from a public URL. Does not send. Exactly one of file_path / url. Maximum 100 MB. Show the preview; after the user says yes, call confirm_send. A GIF is sent with as_gif: an mp4 goes out looping, a .gif is converted to mp4 first (needs ffmpeg on the machine running wazap).

ParametersJSON Schema
NameRequiredDescriptionDefault
urlNoPublic http(s) URL to fetch and send
as_gifNoSend a .gif or an mp4 as a looping GIF, the way WhatsApp plays them
captionNoText shown under the media
chat_idYesChat id as returned by another tool ("<digits>@s.whatsapp.net" or "<id>@g.us"), or a phone number in international format
as_voiceNoSend an audio file as a voice note (push-to-talk)
file_pathNoAbsolute path of a local file to send
account_idNoRegistry account id (default, work, …). Omit to resolve from chat_id or message_id, or the default account.
as_documentNoSend as a plain document instead of rendered media

TDQS

A4.8/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations declare mutation and non-destructiveness but say nothing about limits or side effects; the description supplies that the call only drafts, caps at 100 MB, and requires ffmpeg on the host for GIF conversion (.gif converted to mp4, mp4 loops). This is exactly the extra behavioral context annotations cannot carry.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loaded with the core action and source options, then the draft/confirm workflow, then limits and format detail. Every sentence carries information, though the GIF/ffmpeg clause is dense enough to be slightly hard to parse on first read.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With no output schema and 8 parameters, the description covers the drafting-vs-sending distinction, the mutually exclusive inputs, the size ceiling, dependency requirements, and the handoff to confirm_send. Nothing an agent needs to invoke it correctly is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so the per-field descriptions do the bulk of the work (baseline 3). The description still adds non-obvious constraints absent from the schema: exactly one of file_path/url is required, and the as_gif semantics for mp4 vs .gif. That pushes it above baseline.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb (Draft) plus the resource types (image, video, audio, document, GIF) and the source locations (local path or public URL). It is clearly distinguishable from send_message, download_media, and confirm_send, which all deal with adjacent but different actions.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicitly says 'Does not send' and routes the agent to the follow-up tool: 'after the user says yes, call confirm_send.' It also clarifies the alternation between file_path and url. The when-to-use and what-happens-next are both spelled out.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

send_messageDraft a WhatsApp text messageA

Draft a text message. Does not send. Returns a draft_id and a preview of the recipient and exact text. Show that preview to the user; after they say yes, call confirm_send. A draft lasts 15 minutes.

To @-mention people, pass mention_ids and write @ in the text where each mention belongs, the digits of their id (@40722123456). A mention the text lacks gets its @ added at the end, so the preview is the text that goes out.

ParametersJSON Schema
NameRequiredDescriptionDefault
textYesThe message text
chat_idYesChat id as returned by another tool ("<digits>@s.whatsapp.net" or "<id>@g.us"), or a phone number in international format
reply_toNoQuote-reply to this message
account_idNoRegistry account id (default, work, …). Omit to resolve from chat_id or message_id, or the default account.
mention_idsNoChat ids to @-mention; write @<number> in the text for each, or wazap adds it at the end

TDQS

A4.9/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already signal readOnlyHint=false, openWorldHint=true, idempotentHint=false. The description adds rich behavioral context beyond that: it does not send, it returns a preview, drafts expire after 15 minutes, and mention handling appends missing @<number> tokens. This fully discloses side effects and the exact preview semantics, exceeding what annotations alone convey.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is concise yet complete. The first sentence states the core purpose and the non-sending nature; the next explains the flow and draft expiry; the second paragraph details mention syntax. Every sentence adds operational value, with the most critical information front-loaded.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool's complexity (5 parameters, no output schema), the description fully covers return values (draft_id, preview), the required user confirmation step, draft timeout, and mention behavior. It provides everything an agent needs to call it correctly, including edge cases like missing mentions, so nothing essential is omitted.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the baseline is 3. The description adds value by specifying the @<number> syntax for mentions and clarifying chat_id formats (WhatsApp ID vs phone number). It also explains that the preview is the exact outgoing text, which clarifies the text parameter's role. This goes beyond the schema's field-level descriptions, earning a 4.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description opens with 'Draft a text message' – a specific verb and resource – and immediately clarifies 'Does not send,' distinguishing it from sending tools. It explicitly names the sibling confirm_send and describes its own output (draft_id, preview), so an agent can tell it apart from related tools like send_media or edit_message.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It gives explicit when-to-use guidance: show the preview and only after user approval call confirm_send. This routes the agent to the correct sibling. It also explains the draft's 15-minute lifetime, setting expectations for the workflow. No alternative is left ambiguous.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

send_pollDraft a WhatsApp pollA

Draft a poll. Does not send. Participants vote in WhatsApp, and their votes show on the poll message: read_messages counts them and get_message says who chose each option. Show the preview; after the user says yes, call confirm_send.

ParametersJSON Schema
NameRequiredDescriptionDefault
chat_idYesChat id as returned by another tool ("<digits>@s.whatsapp.net" or "<id>@g.us"), or a phone number in international format
optionsYesAnswer options (2-12)
questionYesThe poll question
account_idNoRegistry account id (default, work, …). Omit to resolve from chat_id or message_id, or the default account.
multi_selectNoAllow voters to pick more than one option

TDQS

A4.4/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

The annotations only signal that this is not read-only, not idempotent, and not destructive. The description adds key behavioral context: the tool creates a draft without sending, poll votes behave in a specific way, and confirmation via confirm_send is required. It could go further by describing the state of the draft or the exact preview mechanism, but it is substantially transparent.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is short, front-loaded with the core purpose, and each sentence contributes meaningful guidance. There is no redundancy or filler.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

The workflow is clear, and the schema covers parameters well. However, there is no output schema and the description does not specify what the preview consists of or how it is returned, which is a notable gap for a draft-generation tool.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the parameters are already fully documented in the schema. The description does not add parameter-specific semantics, but it does not need to; the baseline of 3 applies because the schema carries the load.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description states a specific action, 'Draft a poll', and immediately clarifies 'Does not send', making the boundary of the tool explicit. This clearly distinguishes it from sibling tools like send_message and confirm_send.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It gives explicit workflow guidance: 'Show the preview; after the user says yes, call confirm_send.' It also tells the agent when not to treat this as a send operation, and explains how to interpret poll results using read_messages and get_message.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

set_contact_noteNote something about a contactA
Idempotent

Remember something about a person, on this machine only: "Hermi, my own agent", "the accountant", "always answers late". The note then rides along wherever the contact shows: list_chats, search_contacts, get_contact, get_recent_messages and get_unanswered. Nothing is sent to WhatsApp and the contact never sees it. An empty note removes it.

ParametersJSON Schema
NameRequiredDescriptionDefault
noteYesWhat to remember, or "" to remove the note
account_idNoRegistry account id (default, work, …). Omit to resolve from chat_id or message_id, or the default account.
contact_idYesContact id or phone number

TDQS

A4.4/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Beyond the annotations (which only declare it is a non-read, idempotent, non-destructive write), the description adds high-value behavior: the note is stored on this machine only, nothing is sent to WhatsApp, the contact never sees it, and it propagates to five named tools. These are exactly the traits an agent cannot infer from structured fields.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loaded with purpose, then effect, then privacy, then removal — a logical order with no filler sentences. The inline quoted examples are slightly informal in formatting but earn their place by demonstrating note format.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a 3-parameter tool with full schema coverage and no output schema, the description covers everything an agent needs: local-only scope, cross-tool visibility, privacy, and removal. Nothing material is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so all three parameters are already documented in the schema, including the empty-string removal semantics. The description's examples of note content add mild meaning but do not go beyond what the schema provides, so the baseline of 3 applies.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description states a specific verb and resource (remember/set a note about a person) and immediately differentiates itself by explaining where the note surfaces (list_chats, search_contacts, get_contact, get_recent_messages, get_unanswered). This lets an agent distinguish set_contact_note from sibling read tools without opening any schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It gives concrete context for use via examples ("the accountant", "always answers late") and states the removal condition for empty notes. There is no explicit when-not or named alternative tool, but no sibling competes for this function, so the guidance is clear without exclusions.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

set_profile_pictureSet the linked WhatsApp profile pictureA
Destructive

Set the linked WhatsApp account's own profile picture from a local path on the machine running wazap or from a public URL. Exactly one of file_path / url. JPEG, PNG or WebP only, at most 10 MB. DESTRUCTIVE and visible to every contact. Show the image and wait for a yes first. This call hits WhatsApp immediately; there is no draft.

ParametersJSON Schema
NameRequiredDescriptionDefault
urlNoPublic http(s) URL to fetch and use as the photo
file_pathNoAbsolute path of a local JPEG, PNG or WebP
account_idNoRegistry account id (default, work, …). Omit to resolve from chat_id or message_id, or the default account.

TDQS

A4.7/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare destructiveHint=true, idempotentHint=false and openWorldHint=true, yet the description adds genuinely new context: the picture is 'visible to every contact', the call 'hits WhatsApp immediately', there is 'no draft', plus format and 10 MB size limits. That is real behavioral disclosure beyond the structured hints.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loaded with the core action, then constraints, then the safety workflow. Every sentence earns its place; no padding.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

No output schema exists, so the description carries return expectations on its own, and it does. Source alternation, format/size limits, visibility, immediacy and the confirmation requirement together make this callable and safe without further reference.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100% so the baseline is 3. The description adds the 'Exactly one of file_path / url' mutual-exclusivity rule, which the schema (additionalProperties:false but no oneOf) does not encode, plus format/size constraints. That is added value beyond the schema.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb (set) and resource (the linked account's own profile picture), with the scope 'own' clarifying this is not editing someone else's picture. An agent can distinguish this from send_media or download_media without opening any schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Gives clear operating context: 'Show the image and wait for a yes first' and 'This call hits WhatsApp immediately; there is no draft.' It also states the source constraint. It stops short of naming an alternative tool for a different profile-photo scenario, but the when-to-use confirmation workflow is well specified.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

sync_contactsFetch the phone's address book againA
Read-onlyIdempotent

Ask WhatsApp to send the linked phone's address book from scratch, and wait up to 15 seconds for it. Nothing on WhatsApp changes: this only refills wazap's own contact list.

Use it when get_status reports contacts_named: 0, or when senders in a group read as phone numbers for people you know are saved on the phone. Returns named_before and named_after so you can tell whether it helped; if both are 0 the phone has no saved contacts for these people.

ParametersJSON Schema
NameRequiredDescriptionDefault
account_idNoRegistry account id (default, work, …). Omit to resolve from chat_id or message_id, or the default account.

TDQS

A4.7/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnly/idempotent/non-destructive, and the description adds genuinely new behavioral facts: a bounded up-to-15-second wait, an explicit assurance that nothing on WhatsApp changes, and the names and interpretation of the return fields (named_before/named_after, with both zero meaning the phone has no saved contacts).

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Four tight sentences, front-loaded with the action and its cost (up to 15 seconds), followed by scope, trigger conditions, and return-value interpretation. No filler.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With no output schema, the description compensates by explaining what is returned and how to read it, and it covers trigger conditions, latency, and side-effect scope. An agent has everything needed to call and interpret this correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Only one optional parameter exists and schema description coverage is 100%, so the schema already documents account_id fully. The description adds nothing about parameters, so the baseline of 3 applies.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and resource ('Ask WhatsApp to send the linked phone's address book from scratch') and immediately scopes it ('this only refills wazap's own contact list'), which cleanly separates it from siblings like search_contacts and get_contact that read the existing list.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Gives two explicit triggering conditions ('when get_status reports contacts_named: 0', or 'when senders in a group read as phone numbers for people you know are saved on the phone') and names the sibling tool to check first, leaving no inference needed about when to reach for this.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

transcribe_audioTranscribe a WhatsApp voice messageA
Read-onlyIdempotent

Turn a voice note or an audio message into text. The transcript is cached, so a second call on the same message costs nothing, and from then on the message reads as [voice message · 0:42] "what was said" in read_messages, get_recent_messages and get_message, and its words become searchable through search_messages.

What it costs depends on how the user set transcription up: the local provider (whisper.cpp) is free and the audio never leaves the machine, while the API provider uploads the audio to a third-party service and is billed per minute. Either way this is capped at 10 calls a minute.

TRANSCRIBE_UNAVAILABLE means transcription is off or unfinished on this machine; the fix names the command the user has to run. Do not retry it.

ParametersJSON Schema
NameRequiredDescriptionDefault
languageNoISO 639-1 code of what is spoken, e.g. "ro"; "auto" detects it. Omit to use the configured default.
account_idNoRegistry account id (default, work, …). Omit to resolve from chat_id or message_id, or the default account.
message_idYesA message whose type is voice or audio

TDQS

A4.5/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Goes well past the annotations (readOnly/idempotent/destructive=false) by disclosing caching (a repeat call is free), the cost model (local whisper.cpp free and offline vs API provider uploading audio to a third party and billed per minute), a rate limit (10 calls/minute), and precise error semantics for TRANSCRIBE_UNAVAILABLE with the instruction not to retry. This is exactly the extra behavioral context an agent cannot get from structured fields.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Three tight paragraphs: what it does and its downstream effect first, then cost/privacy/rate limits, then error handling. Every sentence carries load-bearing information (cache, cost, offline vs upload, rate cap, error fix) with no filler.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

No output schema exists, and the description compensates by showing the transcript's rendered form ('[voice message · 0:42] "what was said"') and its searchability. Combined with cost, caching, rate-limit and error coverage, an agent has everything needed to call and interpret this tool.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so language, account_id and message_id are already documented in the schema (defaults, resolution order, ISO codes, accepted message types). The description adds no parameter-specific syntax or format detail beyond that, so the baseline of 3 applies.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb (turn into text) and resource (voice note / audio message), which is a distinct operation from siblings like download_media or read_messages. An agent can immediately tell this is the speech-to-text conversion tool and not a media-download or message-reader.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Clear context for use (voice/audio messages) and it spells out the downstream effect in read_messages, get_recent_messages, get_message and search_messages, plus explicit when-not guidance: do not retry on TRANSCRIBE_UNAVAILABLE. It stops short of naming a concrete alternative tool or a prerequisite chain, so it is clear context rather than fully explicit when/when-not/alternatives.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

update_contact_detailsTag and annotate a contactA
Idempotent

File local, searchable details on a person: tags like "client" or "echipa", and key-value fields like {"role": "contabil", "oras": "Cluj"}. This is how "the accountant" or "all suppliers" resolve later — search_contacts matches tag and field text, and its tag filter lists everyone carrying one. Nothing is sent to WhatsApp: the contact never sees it and the phone is unchanged. A field set to "" is deleted; remove_tags / remove_fields take keys away. Pass at least one of the four edits. The person need not be a saved contact.

ParametersJSON Schema
NameRequiredDescriptionDefault
fieldsNoDetails to set, e.g. {"role": "contabil"}; an empty value deletes the key
add_tagsNoTags to file under, e.g. ["client", "echipa"]; lowercase tokens, "#" optional
account_idNoRegistry account id (default, work, …). Omit to resolve from chat_id or message_id, or the default account.
contact_idYesContact id or phone number
remove_tagsNoTags to take off
remove_fieldsNoDetail keys to delete

TDQS

A4.7/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnlyHint=false, destructiveHint=false, and idempotentHint=true. The description adds valuable behavioral context: nothing is sent to WhatsApp, the contact never sees it, the phone is unchanged, and empty field values delete keys. It also clarifies that remove_tags/remove_fields take keys away. This goes beyond the annotations without contradicting them.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is compact yet information-dense. Every sentence earns its place: purpose, search integration, privacy guarantee, deletion semantics, and usage constraint. It is front-loaded with the core purpose and uses concrete examples ('client', 'echipa', {'role': 'contabil'}) to illustrate without bloat.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a tool with 6 parameters, 100% schema coverage, and no output schema, the description covers all essential context: what the tool does, how it integrates with search, privacy implications, deletion behavior, and the minimum-edit requirement. The annotations cover idempotency and non-destructiveness. Nothing critical is missing for an agent to invoke it correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the schema already documents all parameters. The description adds meaning by explaining the purpose of tags and fields (how they resolve in search_contacts), the deletion semantics for empty values, and the requirement to pass at least one edit. This complements the schema rather than repeating it.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool's purpose: file local, searchable details on a person via tags and key-value fields. It uses specific verbs ('file', 'set', 'delete') and distinguishes itself from related tools like search_contacts and remove_tags/remove_fields. The title 'Tag and annotate a contact' reinforces the purpose without being a tautology.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description explicitly explains when to use this tool: to add searchable metadata to a contact, and how it relates to search_contacts ('this is how 'the accountant' or 'all suppliers' resolve later'). It also states constraints: pass at least one of the four edits, and the person need not be a saved contact. This provides clear usage context and differentiates from siblings.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

wait_for_messagesWait for new WhatsApp messagesA
Read-onlyIdempotent

Block until a message arrives, then return it, or return empty when the timeout passes. This is how an agent stays on the line without polling: call it in a loop, and pass the cursor it returns into the next call so nothing that landed between two calls is missed. The first matching message starts a one-second settle so a burst comes back together.

Only messages from other people are returned, never the user's own, and never WhatsApp's system notices. With addressed_to_me, only direct messages, group messages that @-mention the user, and replies to the user's own messages wake the wait; everything else in a group is ignored. A cursor from a previous run of wazap cannot be honoured: the wait then starts from now and says cursor_reset.

The timeout is capped at 55 seconds because MCP clients give up at 60.

ParametersJSON Schema
NameRequiredDescriptionDefault
cursorNoThe cursor returned by the previous call
chat_idNoOnly messages in this chat
account_idNoRegistry account id (default, work, …). Omit to resolve from chat_id or message_id, or the default account.
addressed_to_meNoOnly direct messages, @-mentions of the user and replies to the user's messages
timeout_secondsNoHow long to wait (1-55 s)

TDQS

A4.6/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

The description adds substantial behavioral context beyond the annotations: it blocks until a message arrives, returns empty on timeout, returns only messages from other people, ignores WhatsApp system notices, supports addressed_to_me filtering, resets cursors from previous runs, and caps timeouts at 55 seconds due to MCP client limits. These are exactly the operational details an agent needs and are not covered by the readOnly/idempotent annotations.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The core purpose is stated first, followed by usage, filtering behavior, cursor rules, and timeout limits. Every sentence adds a distinct operational detail, and there is no redundant or filler content.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the lack of an output schema, the description explains what is returned (a message or empty) and how cursors work. Annotations cover safety and openness, while the description covers blocking semantics, filtering, cursor reset, and timeout constraints. Nothing essential for correct use appears missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Although schema description coverage is 100%, the description adds meaningful semantics beyond the schema: cursor must be passed from the previous call and cannot be honored across runs, addressed_to_me has specific wake conditions, and the timeout cap is explained by MCP client limits. This meaningfully improves correct invocation.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description states a specific blocking behavior: wait until a message arrives, return it, or return empty on timeout. It distinguishes itself from polling-based siblings by explaining it is how an agent stays on the line without polling, though it does not name specific sibling tools like read_messages or get_recent_messages.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It gives clear operational guidance: call in a loop, pass the cursor from the previous call, and rely on the timeout. It explains filtering behavior and cursor reset conditions, but does not explicitly name alternative tools or state when not to use this tool versus read_messages.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

  1. 13 tool updatesv0.21.0
    • Changeddelete_message3 fields changed
      • removedInput schema / properties / for_everyone / default
        Removed value: -false
      • changedInput schema / properties / for_everyone / description
        Previous value: -"Retract for all participants (WhatsApp supports no other kind of delete here)"New value: +"Required. true retracts it for everyone in the chat; false deletes it for the linked account only"
      • changedInput schema / required
        Previous value: -[
        -  "message_id"
        -]New value: +[
        +  "message_id",
        +  "for_everyone"
        +]
    • Changedget_recent_messages1 field changed
      • changedInput schema / properties / types / items / enum
        Previous value: -[
        -  "text",
        -  "image",
        -  "video",
        -  "audio",
        -  "voice",
        -  "document",
        -  "sticker",
        -  "location",
        -  "contact",
        -  "poll",
        -  "reaction",
        -  "deleted",
        -  "view_once",
        -  "call",
        -  "system",
        -  "unknown"
        -]New value: +[
        +  "text",
        +  "image",
        +  "video",
        +  "audio",
        +  "voice",
        +  "document",
        +  "sticker",
        +  "location",
        +  "contact",
        +  "poll",
        +  "reaction",
        +  "deleted",
        +  "view_once",
        +  "call",
        +  "event",
        +  "invite",
        +  "system",
        +  "unknown"
        +]
    • Addedjoin_group
    • Changedmanage_chat3 fields changed
      • changedInput schema / properties / action / enum
        Previous value: -[
        -  "archive",
        -  "unarchive",
        -  "pin",
        -  "unpin",
        -  "mute",
        -  "unmute",
        -  "mark_read",
        -  "mark_unread"
        -]New value: +[
        +  "archive",
        +  "unarchive",
        +  "pin",
        +  "unpin",
        +  "mute",
        +  "unmute",
        +  "mark_read",
        +  "mark_unread",
        +  "pin_message",
        +  "unpin_message",
        +  "star_message",
        +  "unstar_message",
        +  "clear",
        +  "delete",
        +  "block",
        +  "unblock"
        +]
      • addedInput schema / properties / message_id
        Added value: +{
        +  "description": "The message for pin_message, unpin_message, star_message and unstar_message; it must be in chat_id",
        +  "minLength": 5,
        +  "type": "string"
        +}
      • addedInput schema / properties / pin_hours
        Added value: +{
        +  "description": "How long pin_message keeps the message pinned: 24, 168 (default) or 720 hours",
        +  "enum": [
        +    24,
        +    168,
        +    720
        +  ],
        +  "type": "number"
        +}
    • Changedmanage_group5 fields changed
      • changedInput schema / properties / action / enum
        Previous value: -[
        -  "add",
        -  "remove",
        -  "promote",
        -  "demote",
        -  "leave",
        -  "set_subject",
        -  "set_description",
        -  "get_invite_link",
        -  "revoke_invite_link"
        -]New value: +[
        +  "add",
        +  "remove",
        +  "promote",
        +  "demote",
        +  "leave",
        +  "set_subject",
        +  "set_description",
        +  "set_picture",
        +  "remove_picture",
        +  "get_invite_link",
        +  "revoke_invite_link",
        +  "list_join_requests",
        +  "approve_join_requests",
        +  "reject_join_requests",
        +  "set_announcement_only",
        +  "set_info_locked",
        +  "set_add_mode",
        +  "set_join_approval",
        +  "set_disappearing"
        +]
      • addedInput schema / properties / file_path
        Added value: +{
        +  "description": "set_picture: absolute path of a local JPEG, PNG or WebP",
        +  "minLength": 1,
        +  "type": "string"
        +}
      • changedInput schema / properties / participant_ids / description
        Previous value: -"Targets of add/remove/promote/demote"New value: +"Targets of add/remove/promote/demote/approve_join_requests/reject_join_requests"
      • addedInput schema / properties / url
        Added value: +{
        +  "description": "set_picture: public http(s) URL to fetch and use as the photo",
        +  "format": "uri",
        +  "type": "string"
        +}
      • changedInput schema / properties / value / description
        Previous value: -"New subject or description"New value: +"New subject or description; \"on\"/\"off\" for set_announcement_only, set_info_locked, set_join_approval; \"admins\"/\"all\" for set_add_mode; \"off\"/\"24h\"/\"7d\"/\"90d\" for set_disappearing"
    • Changedread_messages1 field changed
      • changedInput schema / properties / types / items / enum
        Previous value: -[
        -  "text",
        -  "image",
        -  "video",
        -  "audio",
        -  "voice",
        -  "document",
        -  "sticker",
        -  "location",
        -  "contact",
        -  "poll",
        -  "reaction",
        -  "deleted",
        -  "view_once",
        -  "call",
        -  "system",
        -  "unknown"
        -]New value: +[
        +  "text",
        +  "image",
        +  "video",
        +  "audio",
        +  "voice",
        +  "document",
        +  "sticker",
        +  "location",
        +  "contact",
        +  "poll",
        +  "reaction",
        +  "deleted",
        +  "view_once",
        +  "call",
        +  "event",
        +  "invite",
        +  "system",
        +  "unknown"
        +]
    • Addedrecall
    • Addedremove_contact
    • Addedsave_contact
    • Changedsearch_contacts3 fields changed
      • changedInput schema / properties / query / description
        Previous value: -"Name fragment or phone number (at least 2 characters)"New value: +"Name fragment, phone number, or tag/detail text (at least 2 characters). Omit with tag to list everyone carrying it."
      • addedInput schema / properties / tag
        Added value: +{
        +  "description": "Only contacts filed under this tag (\"client\"); \"#\" optional",
        +  "minLength": 1,
        +  "type": "string"
        +}
      • removedInput schema / required
        Removed value: -[
        -  "query"
        -]
    • Changedsearch_messages1 field changed
      • changedInput schema / properties / from / description
        Previous value: -"Only messages this person sent: \"me\", a contact id or a phone number"New value: +"Only messages this person sent: \"me\", a phone number, a contact/chat id, or a name that resolves to exactly one person (the error names the candidates when it does not)"
    • Changedsend_message1 field changed
      • changedInput schema / properties / mention_ids / description
        Previous value: -"Chat ids to @-mention; include their names in the text yourself"New value: +"Chat ids to @-mention; write @<number> in the text for each, or wazap adds it at the end"
    • Addedupdate_contact_details
  2. 33 tool updatesv0.15.0
    • Addedconfirm_send
    • Changedcreate_group1 field changed
      • addedInput schema / properties / account_id
        Added value: +{
        +  "description": "Registry account id (default, work, …). Omit to resolve from chat_id or message_id, or the default account.",
        +  "minLength": 1,
        +  "type": "string"
        +}
    • Changeddelete_message1 field changed
      • addedInput schema / properties / account_id
        Added value: +{
        +  "description": "Registry account id (default, work, …). Omit to resolve from chat_id or message_id, or the default account.",
        +  "minLength": 1,
        +  "type": "string"
        +}
    • Changeddownload_media1 field changed
      • addedInput schema / properties / account_id
        Added value: +{
        +  "description": "Registry account id (default, work, …). Omit to resolve from chat_id or message_id, or the default account.",
        +  "minLength": 1,
        +  "type": "string"
        +}
    • Changededit_message1 field changed
      • addedInput schema / properties / account_id
        Added value: +{
        +  "description": "Registry account id (default, work, …). Omit to resolve from chat_id or message_id, or the default account.",
        +  "minLength": 1,
        +  "type": "string"
        +}
    • Changedforward_message1 field changed
      • addedInput schema / properties / account_id
        Added value: +{
        +  "description": "Registry account id (default, work, …). Omit to resolve from chat_id or message_id, or the default account.",
        +  "minLength": 1,
        +  "type": "string"
        +}
    • Changedget_contact1 field changed
      • addedInput schema / properties / account_id
        Added value: +{
        +  "description": "Registry account id (default, work, …). Omit to resolve from chat_id or message_id, or the default account.",
        +  "minLength": 1,
        +  "type": "string"
        +}
    • Changedget_group_info1 field changed
      • addedInput schema / properties / account_id
        Added value: +{
        +  "description": "Registry account id (default, work, …). Omit to resolve from chat_id or message_id, or the default account.",
        +  "minLength": 1,
        +  "type": "string"
        +}
    • Changedget_message1 field changed
      • addedInput schema / properties / account_id
        Added value: +{
        +  "description": "Registry account id (default, work, …). Omit to resolve from chat_id or message_id, or the default account.",
        +  "minLength": 1,
        +  "type": "string"
        +}
    • Changedget_recent_messages3 fields changed
      • addedInput schema / properties / account_id
        Added value: +{
        +  "description": "Registry account id (default, work, …). Omit to resolve from chat_id or message_id, or the default account.",
        +  "minLength": 1,
        +  "type": "string"
        +}
      • addedInput schema / properties / compact
        Added value: +{
        +  "default": false,
        +  "description": "Leave out media without a caption and messages with no words in them, fold what one person sent in a row into one line, and say per chat what was left out. About half the size; use it for a routine catch-up",
        +  "type": "boolean"
        +}
      • addedInput schema / properties / include_previews
        Added value: +{
        +  "default": false,
        +  "description": "Attach a small JPEG of each photo, newest first, up to 12 per call, so you can see what was sent: the preview WhatsApp shipped when there is one, otherwise the photo is downloaded once and shrunk on the machine running wazap",
        +  "type": "boolean"
        +}
    • Changedget_status2 fields changed
      • addedInput schema / additionalProperties
        Added value: +false
      • addedInput schema / properties / account_id
        Added value: +{
        +  "description": "Registry account id (default, work, …). Omit to resolve from chat_id or message_id, or the default account.",
        +  "minLength": 1,
        +  "type": "string"
        +}
    • Addedget_stories
    • Addedget_unanswered
    • Changedlearn2 fields changed
      • addedInput schema / additionalProperties
        Added value: +false
      • addedInput schema / properties / account_id
        Added value: +{
        +  "description": "Registry account id (default, work, …). Omit to resolve from chat_id or message_id, or the default account.",
        +  "minLength": 1,
        +  "type": "string"
        +}
    • Addedlink_account
    • Addedlist_accounts
    • Changedlist_chats1 field changed
      • addedInput schema / properties / account_id
        Added value: +{
        +  "description": "Registry account id (default, work, …). Omit to resolve from chat_id or message_id, or the default account.",
        +  "minLength": 1,
        +  "type": "string"
        +}
    • Changedmanage_chat1 field changed
      • addedInput schema / properties / account_id
        Added value: +{
        +  "description": "Registry account id (default, work, …). Omit to resolve from chat_id or message_id, or the default account.",
        +  "minLength": 1,
        +  "type": "string"
        +}
    • Changedmanage_group1 field changed
      • addedInput schema / properties / account_id
        Added value: +{
        +  "description": "Registry account id (default, work, …). Omit to resolve from chat_id or message_id, or the default account.",
        +  "minLength": 1,
        +  "type": "string"
        +}
    • Addedmark_handled
    • Changedreact_to_message1 field changed
      • addedInput schema / properties / account_id
        Added value: +{
        +  "description": "Registry account id (default, work, …). Omit to resolve from chat_id or message_id, or the default account.",
        +  "minLength": 1,
        +  "type": "string"
        +}
    • Changedread_messages2 fields changed
      • addedInput schema / properties / account_id
        Added value: +{
        +  "description": "Registry account id (default, work, …). Omit to resolve from chat_id or message_id, or the default account.",
        +  "minLength": 1,
        +  "type": "string"
        +}
      • addedInput schema / properties / include_previews
        Added value: +{
        +  "default": false,
        +  "description": "Attach a small JPEG of each photo, newest first, up to 12 per call, so you can see what was sent: the preview WhatsApp shipped when there is one, otherwise the photo is downloaded once and shrunk on the machine running wazap",
        +  "type": "boolean"
        +}
    • Changedsearch_contacts1 field changed
      • addedInput schema / properties / account_id
        Added value: +{
        +  "description": "Registry account id (default, work, …). Omit to resolve from chat_id or message_id, or the default account.",
        +  "minLength": 1,
        +  "type": "string"
        +}
    • Changedsearch_messages4 fields changed
      • addedInput schema / properties / account_id
        Added value: +{
        +  "description": "Registry account id (default, work, …). Omit to resolve from chat_id or message_id, or the default account.",
        +  "minLength": 1,
        +  "type": "string"
        +}
      • addedInput schema / properties / from
        Added value: +{
        +  "description": "Only messages this person sent: \"me\", a contact id or a phone number",
        +  "minLength": 1,
        +  "type": "string"
        +}
      • addedInput schema / properties / since
        Added value: +{
        +  "description": "Only messages from this moment on: a date (\"2026-09-01\") or an ISO timestamp",
        +  "minLength": 4,
        +  "type": "string"
        +}
      • addedInput schema / properties / until
        Added value: +{
        +  "description": "Only messages up to this moment: a date or an ISO timestamp",
        +  "minLength": 4,
        +  "type": "string"
        +}
    • Changedsend_location1 field changed
      • addedInput schema / properties / account_id
        Added value: +{
        +  "description": "Registry account id (default, work, …). Omit to resolve from chat_id or message_id, or the default account.",
        +  "minLength": 1,
        +  "type": "string"
        +}
    • Changedsend_media2 fields changed
      • addedInput schema / properties / account_id
        Added value: +{
        +  "description": "Registry account id (default, work, …). Omit to resolve from chat_id or message_id, or the default account.",
        +  "minLength": 1,
        +  "type": "string"
        +}
      • addedInput schema / properties / as_gif
        Added value: +{
        +  "default": false,
        +  "description": "Send a .gif or an mp4 as a looping GIF, the way WhatsApp plays them",
        +  "type": "boolean"
        +}
    • Changedsend_message1 field changed
      • addedInput schema / properties / account_id
        Added value: +{
        +  "description": "Registry account id (default, work, …). Omit to resolve from chat_id or message_id, or the default account.",
        +  "minLength": 1,
        +  "type": "string"
        +}
    • Changedsend_poll1 field changed
      • addedInput schema / properties / account_id
        Added value: +{
        +  "description": "Registry account id (default, work, …). Omit to resolve from chat_id or message_id, or the default account.",
        +  "minLength": 1,
        +  "type": "string"
        +}
    • Addedset_contact_note
    • Addedset_profile_picture
    • Changedsync_contacts2 fields changed
      • addedInput schema / additionalProperties
        Added value: +false
      • addedInput schema / properties / account_id
        Added value: +{
        +  "description": "Registry account id (default, work, …). Omit to resolve from chat_id or message_id, or the default account.",
        +  "minLength": 1,
        +  "type": "string"
        +}
    • Changedtranscribe_audio1 field changed
      • addedInput schema / properties / account_id
        Added value: +{
        +  "description": "Registry account id (default, work, …). Omit to resolve from chat_id or message_id, or the default account.",
        +  "minLength": 1,
        +  "type": "string"
        +}
    • Addedwait_for_messages
  3. 4 tool updatesv0.10.0
    • Changedget_recent_messages2 fields changed
      • addedInput schema / properties / include_system
        Added value: +{
        +  "default": false,
        +  "description": "Include WhatsApp's own system notices, which are excluded from the bodies and the counts by default",
        +  "type": "boolean"
        +}
      • addedInput schema / properties / types
        Added value: +{
        +  "description": "Keep only these message types; omit for every type. The limit counts matching messages, so [\"call\"] returns that many calls, not that many messages of which some are calls.",
        +  "items": {
        +    "enum": [
        +      "text",
        +      "image",
        +      "video",
        +      "audio",
        +      "voice",
        +      "document",
        +      "sticker",
        +      "location",
        +      "contact",
        +      "poll",
        +      "reaction",
        +      "deleted",
        +      "view_once",
        +      "call",
        +      "system",
        +      "unknown"
        +    ],
        +    "type": "string"
        +  },
        +  "type": "array"
        +}
    • Changedread_messages1 field changed
      • addedInput schema / properties / types
        Added value: +{
        +  "description": "Keep only these message types; omit for every type. The limit counts matching messages, so [\"call\"] returns that many calls, not that many messages of which some are calls.",
        +  "items": {
        +    "enum": [
        +      "text",
        +      "image",
        +      "video",
        +      "audio",
        +      "voice",
        +      "document",
        +      "sticker",
        +      "location",
        +      "contact",
        +      "poll",
        +      "reaction",
        +      "deleted",
        +      "view_once",
        +      "call",
        +      "system",
        +      "unknown"
        +    ],
        +    "type": "string"
        +  },
        +  "type": "array"
        +}
    • Addedsync_contacts
    • Addedtranscribe_audio
  4. 22 tool updatesv0.9.3
    • First observedcreate_group
    • First observeddelete_message
    • First observeddownload_media
    • First observededit_message
    • First observedforward_message
    • First observedget_contact
    • First observedget_group_info
    • First observedget_message
    • First observedget_recent_messages
    • First observedget_status
    • First observedlearn
    • First observedlist_chats
    • First observedmanage_chat
    • First observedmanage_group
    • First observedreact_to_message
    • First observedread_messages
    • First observedsearch_contacts
    • First observedsearch_messages
    • First observedsend_location
    • First observedsend_media
    • First observedsend_message
    • First observedsend_poll

TDQS

A4.1/5.0

Scored across 38 tools

Disambiguation5/5

Each tool maps to a clearly distinct resource and action: search_messages and recall explicitly separate exact-string search from semantic search, the send_* tools differ by payload type, and the retrieval tools (read_messages, get_recent_messages, get_unanswered, wait_for_messages) have well-defined scopes. The only dense areas are manage_chat and manage_group, but their action lists are clearly separated by resource and described precisely.

Naming Consistency4/5

Names overwhelmingly follow a snake_case action_noun pattern — get_message, send_media, create_group, join_group, list_chats, delete_message — with pluralization sensibly matching the resource cardinality. Minor deviations like learn, recall, and mark_handled break the strict verb-noun pattern, but they are rare and still intuitive.

Tool Count3/5

38 tools is well above the 15-tool sweet spot and makes the surface area heavy for an agent to explore. However, the server covers an unusually broad domain — messages, media, contacts, groups, stories, accounts, and search — and nearly every tool earns its place; the compressed manage_chat and manage_group tools keep the count from being even higher.

Completeness4/5

The tool set covers the core WhatsApp workflows well: message send/read/edit/delete/react/forward, media download and transcription, contact management, group admin, searching, stories, and account linking. The main gaps are the absence of a group deletion/disband operation and no explicitly described reply/quote option when drafting a message, but most workflows can still be completed without dead-ending.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Related MCP Connectors

  • WhatsMCP connects Claude and other MCP-compatible AI agents directly to WhatsApp. Send and receive text, images, documents, and voice notes; manage groups (create, add/remove members, promote admins); look up contacts and profiles; follow channels; and read call and message history — all through a standard MCP interface. For voice use cases, WhatsMCP offers SIP-based calling plans (inbound-only, or full inbound/outbound) so AI voice agents can answer and place WhatsApp calls, plus low-latency WebSocket integrations with voice agent providers like ElevenLabs. Multiple WhatsApp accounts can be paired and managed per workspace, with webhook support for real-time inbound message delivery to your own infrastructure.

  • Drive WhatsApp from any MCP client: pair devices, send text and media, manage contacts and groups.

  • Your own WhatsApp as an MCP server: read, search and send from any MCP client.

  • MCP server connecting AI agents to 100+ apps (Gmail, Slack, Notion, GitHub) via one-click OAuth.

Related MCP Servers

  • F
    license
    Not graded
    quality
    B
    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.
    22
    -
  • A
    license
    Not graded
    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.
    2
    MIT
  • A
    license
    Not graded
    quality
    C
    maintenance
    MCP server that connects AI agents to WhatsApp using the multi-device API, enabling messaging, group management, and more as a regular user.
    3 npm
    MIT
  • A
    license
    Not graded
    quality
    A
    maintenance
    A native MCP server for SocialMate that gives your AI a WhatsApp, enabling it to send and read messages, manage contacts and groups, and more through 44 tools.
    27 npm
    1
    MIT