wacli.me alpha

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 progress
list_chatsChats by recent activity
list_messagesMessages, filtered by chat, sender and time
search_messagesFull-text search over synced history
search_contactsName or number to JID
get_chatOne chat in detail
get_message_contextWhat surrounded a message
list_starredMessages the owner starred
list_callsCall events, answered and missed
list_groupsGroups the account belongs to
get_groupSubject, description, participants, admins
history_coverageHow far back history reaches per chat
get_mediaFetch one message's attachment
send_messageSend text, including to a number never messaged before
check_numbers_on_whatsappIs this number on WhatsApp at all?
react_to_messageEmoji reaction, visible to the chat
mark_chat_readClear unread, sends read receipts

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

ArgumentTypeMeaning
limitnumber1–200, default 25
querystringFilter by chat name
unreadbooleanOnly 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.

ArgumentTypeMeaning
chatstringChat JID; omit for all chats
limitnumber1–200, default 25
from_mebooleantrue only your messages, false only received
after / beforestringYYYY-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.

ArgumentTypeMeaning
querystringSearch terms, required
limitnumber1–200, default 25
chatstringRestrict to one conversation
after / beforestringTime 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.

ArgumentTypeMeaning
querystringName fragment or number, required
limitnumber1–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.

ArgumentTypeMeaning
chatstringChat 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.

ArgumentTypeMeaning
chatstringChat JID, required
message_idstringId from a search or list result
before / afternumber0–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”.

ArgumentTypeMeaning
groupstringGroup 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.

ArgumentTypeMeaning
chatstringChat JID, required
message_idstringId 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.

ArgumentTypeMeaning
tostringJID, phone number, or exact chat name
messagestringMessage 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.

ArgumentTypeMeaning
numbersstring[]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.

ArgumentTypeMeaning
tostringChat JID, number or exact name
message_idstringTarget message
reactionstringEmoji, default 👍; empty string removes
senderstringSender 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: .