Tools — Inbox & Messaging
MCP tools to read the unified inbox, mark email threads, and send messages across Email, LinkedIn, and WhatsApp.
The Inbox & Messaging tools give your AI assistant read access to the unified inbox (Email, LinkedIn, WhatsApp) and the ability to mark email threads and send messages on your connected channels.
Visibility is exactly what you see in the app. The inbox tools mirror the app's visibility model — there is no extra token scope to unlock content. list_threads and get_thread only ever return threads you own, and the owner-bypass means you read your own message bodies (decrypted at rest, exactly as in the app's inbox). The previous inbox:content scope no longer exists.
crm_inbox
Read email and message conversations from the unified inbox (read-only).
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
action | list_threads | get_thread | get_interaction | Yes | Operation to perform. |
channel | all | email | meeting | linkedin | whatsapp | No | Filter threads by channel (list_threads). Defaults to all. |
sort_by | string | No | Thread field to sort by (list_threads). Defaults to last_message_date. |
sort_dir | asc | desc | No | Sort direction (list_threads). Defaults to desc. |
cursor | string | No | Pagination cursor for thread listing (list_threads). |
limit | number | No | Max threads per page. Default 20, max 100. |
thread_id | string | No | Thread UUID. Required for get_thread. |
message_cursor | string | No | Pagination cursor for messages within a thread (get_thread). |
message_limit | number | No | Max messages per page (get_thread). Default 50, max 200. |
interaction_id | string | No | Single message UUID. Required for get_interaction. |
Key behaviors
- App-parity visibility — You only ever see threads you own. Email/meeting threads are owned via the connected account that synced them; LinkedIn/WhatsApp threads are owned via the thread's managed-by-workspace-user. A non-owned single message fetched via
get_interactionis gated by that thread's content visibility. - Decrypted bodies — Message bodies are decrypted at read time for threads you own, so you get the full content just like in the app.
- Thread pagination — Listing is paginated server-side. Use
cursorandlimitto page through results. - Message pagination — Large threads paginate internally via
message_cursorandmessage_limit. Whennext_message_cursorisnull, all messages have been returned.
Thread detail folds in the former crm_interaction_detail tool: use action: "get_thread" for a whole conversation, or action: "get_interaction" for one message. The standalone activity feed moved to the Data page as crm_find_activity.
Examples
List the most recent email threads:
{
"action": "list_threads",
"channel": "email",
"limit": 10
}Response:
{
"data": [
{
"id": "t_abc123",
"subject": "Q3 contract renewal",
"channel": "email",
"last_message_date": "2026-06-10T14:22:00Z"
}
],
"total": 23,
"nextCursor": "eyJvZmZzZXQiOjEwLCJsaW1pdCI6MTB9"
}Get a thread with paginated messages:
{
"action": "get_thread",
"thread_id": "t_abc123",
"message_limit": 20
}Response:
{
"thread": {
"id": "t_abc123",
"subject": "Q3 contract renewal",
"channel": "email"
},
"messages": [
{
"id": "msg_001",
"date": "2026-06-10T14:22:00Z",
"body": "Hi Bob, following up on the renewal..."
}
],
"total_messages": 4,
"next_message_cursor": null
}Get a single message by id:
{
"action": "get_interaction",
"interaction_id": "550e8400-e29b-41d4-a716-446655440000"
}crm_mark_email
Mark an email thread as read/unread or favorite/unfavorite. Email channel only.
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
action | read | unread | favorite | unfavorite | Yes | read/unread toggle the unread state; favorite/unfavorite toggle the star. |
thread_id | string | Yes | CRM thread id (interaction_threads.id) of an email thread you own. |
Key behaviors
- Email only — These map to Gmail labels (
UNREAD/STARRED) or Outlook flags in your own mailbox. A non-emailthread_idis rejected withINVALID_CHANNEL. - Ownership-checked — You can only act on a thread you own (you must have synced an interaction in it). Otherwise you get
PERMISSION_DENIED. - Provider-resolved — The handler resolves the CRM thread to its external thread id (
external_thread_id) and to the connected account that synced it, then applies the change in the provider mailbox. The CRM stores no read/favorite state — it lives in your mailbox. - Reversible — Every action has its inverse (
read↔unread,favorite↔unfavorite).
Get the thread_id from crm_inbox or crm_find_activity.
Examples
Mark a thread as read:
{
"action": "read",
"thread_id": "t_abc123"
}Response:
{
"thread_id": "t_abc123",
"action": "read",
"channel": "email",
"applied": true
}Star (favorite) a thread:
{
"action": "favorite",
"thread_id": "t_abc123"
}crm_send_message
Send a message on a connected channel — Email, LinkedIn, or WhatsApp.
Mandatory two-step gate (enforced server-side). First call action: "preview" — this resolves the sender account, recipient/thread, and validates prerequisites without sending, returning the exact message that would be sent plus a single-use confirm_token. Present that preview to the user, then call action: "send" — echoing the confirm_token — only after the user explicitly confirms. The send is rejected without a matching token (CONFIRM_REQUIRED), and the token is bound to the exact message previewed (changing a recipient or the body invalidates it → CONFIRM_MISMATCH). Sending is irreversible.
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
action | preview | send | Yes | preview validates and returns the message without sending; send actually sends (only after user confirmation). |
channel | email | linkedin | whatsapp | Yes | Channel to send on. |
body | string | Yes | Message body. For email this is HTML. |
thread_id | string | No | Reply within this existing CRM thread (interaction_threads.id). Required for LinkedIn; optional for email; for WhatsApp it resolves the chat. |
to | string[] | No | Email: recipient addresses. Required for email. |
cc | string[] | No | Email: CC addresses. |
bcc | string[] | No | Email: BCC addresses. |
subject | string | No | Email: subject. Required for email; for a reply, set it yourself (e.g. "Re: ..."). |
from_account_email | string | No | Email: which connected account to send from. Optional if you have exactly one connected account; required if several (preview returns the list). |
chat_id | string | No | WhatsApp: target chat id (e.g. 33XXXXXXXXX@c.us). Not needed when thread_id is given. |
confirm_token | string | On send | The single-use token returned by the matching preview call. Echo it verbatim to authorize sending the exact previewed message. |
Per-channel requirements
Requires to, subject, and body (HTML). from_account_email is auto-picked when you have exactly one connected email account, otherwise specify it (AMBIGUOUS_SENDER lists the available accounts). Send must be enabled on the account (SEND_NOT_ENABLED otherwise — reconnect to grant send permission). Pass thread_id to thread a reply; you must own that email thread.
Reply-only: requires an existing thread_id (a LinkedIn conversation you own, found via crm_inbox) plus body. Requires an active LinkedIn connection.
Provide either thread_id (a WhatsApp thread you own — resolves the chat) or chat_id (e.g. 33XXXXXXXXX@c.us), plus body. Requires an active WhatsApp connection.
Key behaviors
- Own accounts only — You can only send from your own connected accounts, and can only reply within a thread you own. Ownership is re-checked on every send.
- Per-channel validation — Preview surfaces missing/ambiguous inputs as structured errors (
MISSING_REQUIRED_FIELD,AMBIGUOUS_SENDER,NO_ACCOUNT,PERMISSION_DENIED,INVALID_CHANNEL,SEND_NOT_ENABLED) before anything is sent. - Stateful confirmation — The
confirm_tokenfrompreviewis single-use, expires after ~5 minutes, and is bound to the message fingerprint. Asendwith no/expired token returnsCONFIRM_REQUIRED/CONFIRM_INVALID; one whose recipients or body changed since preview returnsCONFIRM_MISMATCH. (If the rate-limit/confirmation store is unavailable the gate fails open so an outage can't block legitimate sends.) - Idempotent — An identical message sent twice in quick succession is de-duplicated server-side: the second call returns
mode: "duplicate_suppressed"and is not delivered again. - Rate limited — Real sends are capped (
api:messaging-send, 30/hour per user); previewing is free.
Examples
Step 1 — preview a new email:
{
"action": "preview",
"channel": "email",
"to": ["bob@acme.com"],
"subject": "Q3 contract renewal",
"body": "<p>Hi Bob, following up on the renewal...</p>",
"from_account_email": "alice@example.com"
}Preview response:
{
"mode": "preview",
"confirm_required": true,
"confirm_token": "a1b2c3d4-…",
"message": {
"channel": "email",
"from": "alice@example.com",
"to": ["bob@acme.com"],
"cc": [],
"bcc": [],
"subject": "Q3 contract renewal",
"body": "<p>Hi Bob, following up on the renewal...</p>",
"reply_to_thread": null
},
"note": "Present this message to the user. Call again with action=\"send\" AND echo this confirm_token ONLY after the user has explicitly confirmed. Sending is irreversible."
}Step 2 — send after the user confirms (echo the confirm_token from step 1):
{
"action": "send",
"channel": "email",
"to": ["bob@acme.com"],
"subject": "Q3 contract renewal",
"body": "<p>Hi Bob, following up on the renewal...</p>",
"from_account_email": "alice@example.com",
"confirm_token": "a1b2c3d4-…"
}Sent response:
{
"mode": "sent",
"channel": "email",
"message_id": "msg_001",
"message": {
"channel": "email",
"from": "alice@example.com",
"to": ["bob@acme.com"],
"subject": "Q3 contract renewal",
"body": "<p>Hi Bob, following up on the renewal...</p>"
}
}Reply on LinkedIn (preview):
{
"action": "preview",
"channel": "linkedin",
"thread_id": "t_li_456",
"body": "Thanks for connecting — happy to set up a call next week."
}Send a WhatsApp message by chat id:
{
"action": "send",
"channel": "whatsapp",
"chat_id": "33612345678@c.us",
"body": "Hi! Just confirming our meeting tomorrow at 10am."
}