tools/call arguments for the flows an autonomous agent actually runs.
Dairo’s MCP surface is split per domain into read and write tools (list_inboxes
and manage_inboxes, read_mailbox and delete_messages); a few primitives stay
standalone (send_message, verify_message, search_docs).
Each block below is { "name": <tool>, "arguments": {...} } — the shape a
tools/call takes.
Recipe 1 — Send from an inbox, then confirm delivery
1a. List inboxes and pick one. Each inbox carries anid (pass it as
inboxId when you send), an address, and a status.
status: "sent" with a
providerMessageId, or a precise error at submit time — never a false “queued”
that fails later.
{ "id": "msg_…", "status": "sent", "providerMessageId": "…", "channel": "email", "warnings": [] }.
A send where every recipient is a Dairo inbox is delivered status: "sent" over
channel: "a2a" with a provenance receipt instead.
1c. Diagnose — the one call to reach for when a send misbehaves. Read-only,
messages:read, and A2A-aware.
renderStatus, providerStatus, domainVerification,
recipientEligibility, deliveryEvents, and a precise nextAction.
1d. Fetch delivery events. Two surfaces — reach for the right one.
Per-message timeline (messages:read):
events:read) — filter by the same
idempotencyKey you sent with to correlate the send to its events:
The ledger defaults to oldest-first — it is a forward catch-up and replay stream.
Pass
order: "newest" to see the most recent event on page 1. On an unfiltered
page, gaps: [] is the healthy signal (no lost events in the window), not
“detection skipped”.Recipe 2 — Receive a message, fetch the thread, reply idempotently
2a. Find new inbound messages.messages[], each with
from.address, subject, direction, and textBody.
from.address. Make it safe against retries with a deterministic
idempotencyKey derived from the inbound message id — re-running the call returns
the original send verbatim instead of double-replying.
Recipe 3 — Resolve a contact and send to it
Contacts are a channel-agnostic address book: one identity, many handles, plus a free-textinfo note.
3a. Create a contact (mutation, so confirm: true, scope contacts:write).
contactId: "me" for the project self-contact):
to[] use "@alias", "contact:<uuid>", or "@me" — Dairo
resolves the contact’s primary handle for the sending inbox’s channel
automatically. send_message also takes a top-level contactId field that does
the same resolution.
messages action, contacts:read) — every
inbound and outbound message with this contact across all channels, merged
newest-first, so an agent can catch up on a relationship before it replies. Each
item carries its channel and direction; page with limit and cursor.
Recipe 4 — Send with an attachment
Each attachment is an object, and each item is exactly one ofcontentBase64
(inline) or objectId (storage reference) — never both, never neither. Up to 10
attachments.
4a. Inline — small files only (≤ 8 MiB decoded per attachment and ≤ 8 MiB total
inline). filename is required.
objectId — the way to attach anything larger (native-attached up to
24 MiB, fetched server-side, ownership-checked). First upload:
{ "objectId": "obj_…", "uploadUrl": "…", "method": "PUT", "headers": {…}, "expiresInSeconds": … }.
PUT the raw bytes to uploadUrl with exactly those headers yourself. To confirm
the object finalized before you attach it, poll the read tool list_storage with
{ "action": "getUploadStatus", "bucketId": "buk_…", "objectId": "obj_…" }. Then
attach by id — filename and contentType are derived from the object unless you
override them:
Recipe 5 — Interactive Telegram: buttons, tap, edit
On a Telegram-channel inbox,send_message takes a buttons
grid, taps come back as inbound events, and manage_sent_messages edits or reacts
in place.
5a. Send with an inline keyboard. buttons is a 2-D array (rows of buttons);
each button is exactly one of url (opens a link) or callback (surfaces the
tap).
subject: "Button tapped: Approve", textBody set to your callback value) and
fires a message.button.tapped webhook. Read it with the mailbox tool, or
subscribe to the event. Match sourceMessageId to your 5a send and branch on
buttonCallback:
text or
html. Omit buttons to keep the current keyboard, pass [] to clear it, or pass
a new grid to replace it. Clearing the keyboard on completion stops a second tap.
emoji must be a Telegram-allowed reaction; unreact clears it.
@username in to to send by
username instead of the bound chat.
Recipe 6 — Send a compliant physical letter (no postal knowledge needed)
Physical mail has a layout contract (address window, keep-out zones) that the platform teaches you — never guess it. 6a. Get the spec + starter templates. One call returns the full layout contract in mm plus compliant, ready-to-store letter HTML:starterTemplates[].html from the
response verbatim (or your own HTML that keeps the documented zones clear):
templateId source
renders with your real recipient and checks the result — pass to so the
printed address is matched against the intended recipient:
{ "valid": true, "checks": [...] }. Fix every fail before
sending; a bring-your-own PDF (pdfBase64) verifies the same way.
6d. Price, then send. Same source shapes; dryRun: true on send is a
full no-send rehearsal (validated + priced, never mailed, never charged):
list_letters { "action": "get", "letterId": "let_…" } — the
status runs queued → submitted → in_transit → delivered.
Sharp edges
Things an agent gets wrong without being told:- Body is a strict one-of. Provide exactly one of
text,html,react, ortemplate. Two or more is a400—"Provide exactly one body source: one of text, html, react, or template".text+htmltogether is rejected; Dairo does not build a multipart alternative. A send with no body is allowed only when it carries an attachment or a Telegram payload (a caption-less file or a poll). template, nottemplateId. Send from a stored template withtemplate: { id, version?, variables }. A template that renders to an empty body fails synchronously (422 template_render_empty, naming the id, version, and variables) before any handoff — not as a silent blank email.- Attachment caps and shape. One object per item; exactly one of
contentBase64(inline, requiresfilename, ≤ 8 MiB each and ≤ 8 MiB total across all inline attachments) orobjectId(≤ 24 MiB). Two 8 MiB inline attachments fail the total cap even though each is within the per-item cap. Over cap →413. Both or neither set →400.delivery: "link"→400; Dairo never auto-edits your body to insert a share link. - Inline attachment breaks A2A. See Recipe 4 — any inline bytes force the send over email.
- Idempotency returns the original, never re-sends. Reusing a key returns the
first send for that key verbatim. Reusing it with a different
subjectortostill returns the original, plus awarnings[]entryreason: "idempotency_key_reused_with_different_params"— use a fresh key for different content. Keys are ≤ 128 characters. - Confirmation gate on mutations. Most mutating write-tool actions require
confirm: true(manage_inboxescreate/delete/setSchema,manage_contactscreate/update/addHandle,replay_event,delete_messages,manage_sent_messagescancelScheduled, allmanage_storagewrites,manage_budgetsset/delete, and the rest). The Telegram edits are the exception:manage_sent_messagesedit,react, andunreacttake noconfirm.send_messagedoes not takeconfirmeither — it is gated by themessages:sendscope. - Suppression safety. Complained recipients are blocked by default; override
only with
ignoreComplaints: truefor a deliberate contact. AlistComplaintshit means do not contact again. A bounce means verify the address before retrying. - Scope is per action, not per tool. Reads use
…:read, writes use…:write;send_messageneedsmessages:send. A key that lacks the scope gets a403. - Recipient limits and resolution.
to + cc + bcc≤ 50. A UUID-shaped recipient resolves to that inbox’s address, org-scoped — another tenant’s UUID will not resolve. Contact refs (@alias,contact:<uuid>,@me) resolve to the primary handle for the sending inbox’s channel. channel: "a2a"cannot be scheduled. A2A delivers synchronously, so pairing it withsendAtis a400— omit one. Whenchannelis omitted, Dairo auto-classifies: A2A when every recipient is a Dairo inbox, otherwise email. A Telegram-channel inbox always delivers over Telegram regardless.- Extra send fields. Beyond the body and recipients,
send_messageadvertisescontactId,channel(emailora2a),replyTo,headers {}, andtags {}, and its top-level schema isadditionalProperties: true, so unknown extras are tolerated.headersis screened by a server-side denylist —From,To,Subject,Reply-To,Message-Id, DKIM,Content-*, andX-Dairo-*are rejected, while any other custom header (includingIn-Reply-ToandReferences) passes through.replyTois validated as a single email address. - Telegram buttons and edits.
buttonsis a 2-D grid (≤ 8 rows × ≤ 8 per row); each button is exactly one ofurl(http,https, ortgonly) orcallback(≤ 200 characters — Dairo stores it and hands it back on the tap, so it never hits Telegram’s 64-bytecallback_datacap).manage_sent_messages { action: "edit" }edits only Telegram messages you sent (else404); on an edit, omittingbuttonspreserves the keyboard,[]clears it.reactneeds a Telegram-allowed emoji (a custom or premium one is a400). Each tap is its own inbound row, plus amessage.button.tappedevent. - MCP never streams raw bytes. Attachment actions return metadata and URLs
only. To read an inbound file, call
read_mailbox { "action": "getAttachmentUrl" }(or"downloadAttachment") and fetch the returned URL yourself.