Tool reference
Seventeen tools: thirteen read-only, four behind the write switch. Each returns JSON as text content, close to what the underlying CLI produced — the only reshaping is unwrapping its result envelope so a failed call arrives as a real error.
account_statusLink state, store size, sync progresslist_chatsChats by recent activitylist_messagesMessages, filtered by chat, sender and timesearch_messagesFull-text search over synced historysearch_contactsName or number to JIDget_chatOne chat in detailget_message_contextWhat surrounded a messagelist_starredMessages the owner starredlist_callsCall events, answered and missedlist_groupsGroups the account belongs toget_groupSubject, description, participants, adminshistory_coverageHow far back history reaches per chatget_mediaFetch one message's attachmentsend_messageSend text, including to a number never messaged beforecheck_numbers_on_whatsappIs this number on WhatsApp at all?react_to_messageEmoji reaction, visible to the chatmark_chat_readClear unread, sends read receiptsaccount_status
account_status() → object
Diagnostics for the linked account. Use it first when something returns nothing: an account that was never linked, or one that logged out, shows up here rather than as an empty chat list.
{
"store_dir": "/srv/stores/<account>",
"authenticated": true,
"linked_jid": "[email protected]",
"lock_held": true,
"messages": 48213
}
A lock_held of true is normal: the sync daemon holds the store lock while it runs.
list_chats
list_chats(limit?, query?, unread?) → array
Chats of the linked account, most recent activity first. This is the usual way to turn a name into a JID.
| Argument | Type | Meaning |
|---|---|---|
| limit | number | 1–200, default 25 |
| query | string | Filter by chat name |
| unread | boolean | Only chats with unread messages |
[
{
"jid": "[email protected]",
"name": "Apartment 4B",
"kind": "group",
"unread": true,
"unread_count": 14,
"last_message_ts": 1789412044,
"pinned": false,
"archived": false
}
]
list_messages
list_messages(chat?, limit?, from_me?, after?, before?) → array
Messages newest first. Without chat it spans every conversation, which is how “what came in today” questions get answered.
| Argument | Type | Meaning |
|---|---|---|
| chat | string | Chat JID; omit for all chats |
| limit | number | 1–200, default 25 |
| from_me | boolean | true only your messages, false only received |
| after / before | string | YYYY-MM-DD or RFC 3339 |
// "what did the landlord write this week?" { "name": "list_messages", "arguments": { "chat": "[email protected]", "after": "2026-09-14", "from_me": false, "limit": 50 } }
search_messages
search_messages(query, limit?, chat?, after?, before?) → array
Full-text search across the synced history, backed by SQLite FTS5 where the store supports it and a LIKE scan otherwise. This is the tool that makes two years of chat useful.
| Argument | Type | Meaning |
|---|---|---|
| query | string | Search terms, required |
| limit | number | 1–200, default 25 |
| chat | string | Restrict to one conversation |
| after / before | string | Time bounds |
// "find the IBAN someone sent me" { "name": "search_messages", "arguments": { "query": "IBAN DE", "limit": 10 } }
search_contacts
search_contacts(query, limit?) → array
Resolve a name or number to a contact and its JID, from the synced contact metadata. Cheaper than listing chats when you already know who you mean.
| Argument | Type | Meaning |
|---|---|---|
| query | string | Name fragment or number, required |
| limit | number | 1–200, default 25 |
get_chat
get_chat(chat) → object
One chat in detail: name, kind, unread count, mute and pin state. Useful when a client already holds a JID and does not want to page through a chat list to check one thing.
| Argument | Type | Meaning |
|---|---|---|
| chat | string | Chat JID, required |
get_message_context
get_message_context(chat, message_id, before?, after?) → object
The messages around one message. A search hit on its own is often meaningless — “yes, fine” answers something, and this is how a model finds out what.
| Argument | Type | Meaning |
|---|---|---|
| chat | string | Chat JID, required |
| message_id | string | Id from a search or list result |
| before / after | number | 0–50 messages each side, default 5 |
// search first, then read around the hit { "name": "get_message_context", "arguments": { "chat": "[email protected]", "message_id": "3EB0…", "before": 8, "after": 4 } }
list_starred
list_starred(limit?, chat?) → array
Messages the account owner starred. A hand-curated set of the things they thought mattered — often the fastest path to an address, a code or a document.
list_calls
list_calls(limit?, chat?, after?, before?) → array
Call events with time, peer and whether the call was answered. Answers “did they ever actually call me back” without reading a single message.
list_groups
list_groups(limit?, query?) → array
Groups the account belongs to, optionally filtered by name. Groups also appear in list_chats; this returns the group-level record instead of the chat record.
get_group
get_group(group) → object
Subject, description, participant list and admins of one group. The tool that answers “who is even in this group” and “who can add people”.
| Argument | Type | Meaning |
|---|---|---|
| group | string | Group JID ending in @g.us |
history_coverage
history_coverage(limit?, query?, chat?) → object
How far back synced history actually reaches, per chat. Worth calling before concluding that something was never said: an empty search can mean “not in this account's synced window” rather than “never happened”.
get_media
get_media(chat, message_id) → image | object
Downloads the attachment of one message on demand. Images up to 5 MB come back as image content, so a model can look at the photo of the parking permit instead of guessing from the filename. Other types return their media type and size.
| Argument | Type | Meaning |
|---|---|---|
| chat | string | Chat JID, required |
| message_id | string | Id of the message holding the media |
Media is fetched per call and the temporary copy is deleted immediately afterwards. Nothing is bulk-downloaded in the background.
Write access
These four exist only for accounts with the write switch on. For everyone else they are not registered, so tools/list does not mention them. Three of them change what other people see — that is the line, not whether a call technically writes to your own store.
send_message write access
send_message(to, message) → object
Sends a text message from the linked account. The tool is not registered at all for accounts without write access — a client cannot list it, let alone call it. Where it is enabled, the write path runs without the CLI's read-only guard, and the result contains the message id that WhatsApp assigned.
| Argument | Type | Meaning |
|---|---|---|
| to | string | JID, phone number, or exact chat name |
| message | string | Message text |
A phone number that has never been messaged works: WhatsApp starts a new conversation, exactly as it would if you typed the number into the app. Check the number exists first.
This one is irreversible. A sent message cannot be unsent by the protocol, and automated sending is what puts a number at risk of restriction. Good clients confirm recipient and wording with you first — insist on one that does.
check_numbers_on_whatsapp write access
check_numbers_on_whatsapp(numbers[]) → array
Asks WhatsApp whether phone numbers are registered, before you write to one of them. This is a live lookup against WhatsApp's servers, not a local database query — which is why it sits behind the write switch and is capped at 20 numbers per call.
| Argument | Type | Meaning |
|---|---|---|
| numbers | string[] | 1–20 numbers in international format |
Check numbers a person actually intends to contact. Sweeping a range through this is number enumeration, it is against WhatsApp's terms, and it is grounds for having the account's access removed here.
react_to_message write access
react_to_message(to, message_id, reaction?, sender?) → object
Puts an emoji reaction on a message; an empty reaction removes it. Everyone in the chat sees it, which makes it a write even though nothing is sent as a message.
| Argument | Type | Meaning |
|---|---|---|
| to | string | Chat JID, number or exact name |
| message_id | string | Target message |
| reaction | string | Emoji, default 👍; empty string removes |
| sender | string | Sender JID of the target — required in groups |
mark_chat_read write access
mark_chat_read(chat) → object
Clears the unread badge. It sends read receipts, so the other side learns you have seen the message — use it after actually reading, not to tidy a list.
Calling a tool without a client
curl -sS https://wacli.me/mcp \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call", "params":{"name":"list_chats","arguments":{"limit":5}}}'
The response arrives as a single server-sent event; the JSON sits after data: .