Skip to main content

MCP Tools Reference

The MCP endpoint exposes 15 tools. This page documents each one, what it returns, and the REST endpoint it dispatches to.

Tools are not a second implementation of the API. Each one wraps a route: the tool's input schema is generated from that route's request validation, and calling the tool runs that route in-process with the caller's own credential. Anything the API Reference says about an endpoint - identifier formats, provider differences, error conditions - is true of the tool that wraps it.

The catalog

ToolWhat it doesBehaviorWraps
list_accountsLists accounts on the instance, pagedread-onlyGET /v1/accounts
get_accountOne account: name, address, connection state, sync statusread-onlyGET /v1/account/{account}
list_mailboxesFolder tree with counters and special-use rolesread-onlyGET /v1/account/{account}/mailboxes
list_messagesMessages in one folder, newest first, pagedread-onlyGET /v1/account/{account}/messages
search_messagesStructured search inside one folderread-onlyPOST /v1/account/{account}/search
get_messageOne message: envelope, flags, attachment list, and the body inlineread-onlyGET /v1/account/{account}/message/{message}
get_message_textA message body on its own, with a larger budget than get_messageread-onlyGET /v1/account/{account}/text/{text}
get_attachmentOne attachment, inline as base64read-onlyGET /v1/account/{account}/attachment/{attachment}
update_messageAdds or removes flags and Gmail labelswritePUT /v1/account/{account}/message/{message}
move_messageMoves a message to another folderwritePUT /v1/account/{account}/message/{message}/move
delete_messageMoves to Trash, or deletes permanently from TrashdestructiveDELETE /v1/account/{account}/message/{message}
create_draftStores a message in a folder without sending itwritePOST /v1/account/{account}/message
send_messageQueues an email for delivery to real recipientssends emailPOST /v1/account/{account}/submit
get_outboxThe sending queue, including scheduled messagesread-onlyGET /v1/outbox
list_templatesStored email templatesread-onlyGET /v1/templates

The Behavior column is what the tool advertises through MCP annotations (readOnlyHint, destructiveHint, openWorldHint). Clients use them to decide what to run without asking and what to confirm first. send_message is the only tool marked as reaching the outside world, because it is the only one that can leave the connected mailboxes.

The same catalog is rendered live in the admin interface, under Configuration > MCP > Connect an agent > Exposed tools. That page reads the running registry, so it is the authority on what the instance in front of you exposes:

The MCP tool catalog in the admin interface The Exposed tools card shows exactly what a client receives from tools/list

Not everything the API can do

The catalog is deliberately small. Creating and deleting folders, editing accounts, exporting mailboxes, managing webhooks, gateways and templates, reading connection logs and everything under settings and credentials have no tools, and an mcp token is refused those operations even if it asks for them by another route. Use the REST API for administration.

A tool schema is narrower than its endpoint

The arguments a tool offers are a curated subset of what the REST route accepts. Three rules shape them, and they are worth knowing before comparing a tool against its endpoint documentation:

  • Hidden arguments. Fields that are an operator's decision rather than an agent's do not appear. send_message offers the message an agent composes and nothing else: no gateway or envelope routing, no trackOpens/trackClicks, no dsn, no headers/messageId, no raw, and no mailMerge - an agent that should write to several people calls the tool several times, so that each send is visible as a send.
  • Forced values. Some arguments are pinned by the server and removed from the schema, so the model is not offered a choice it does not have. Both body tools force sanitized web-safe HTML with a size budget, which is why neither takes textType or maxBytes.
  • Tightened bounds. Every paged listing caps pageSize at 100 rather than the endpoint's 1000. The schema says so, and the dispatch clamps a larger value anyway.

None of this is enforcement. It shapes what the agent is offered; what it is allowed to do is decided by the credential - see Access Control.

Account-bound credentials see simpler tools

When the token is bound to one account, the account argument disappears from every tool that takes one, and EmailEngine fills the binding in on dispatch. The agent is told which account it is working with in the connect instructions, so it never has to look one up. Everything below shows the unbound shape.

Typical agent workflow

The tool descriptions steer a model through this sequence, and the server sends the same guidance as MCP instructions on connect:

  1. list_accounts gives the account id every other tool needs. A bound token skips this step entirely.
  2. list_mailboxes gives folder paths. Paths are what list_messages, search_messages and move_message take, and they are provider-specific strings, not names to guess at.
  3. list_messages or search_messages gives message ids.
  4. get_message gives the envelope, the attachment list and the body in one call. Only when text.hasMore is true is a second call needed, and get_message_text is the one that makes it.
  5. Acting on the message: flags with update_message, filing with move_message, answering with send_message and a reference block.

Arguments

Every tool takes an account argument except list_accounts and get_outbox, which are instance-wide; list_templates takes one optionally, to pick account-specific templates over shared ones. Required arguments are marked.

Accounts and folders

list_accounts

ArgumentTypeNotes
pageintegerZero-indexed
pageSizeintegerEntries per page, at most 100
statestringFilter by connection state, for example connected
querystringSubstring match on the account id, name or address

get_account

ArgumentTypeNotes
account (required)stringAccount id
quotabooleanInclude mailbox quota

Credentials are masked in the response.

list_mailboxes

ArgumentTypeNotes
account (required)stringAccount id
countersbooleanInclude message and unseen counts

Reading

list_messages

ArgumentTypeNotes
account (required)stringAccount id
path (required)stringFolder path, or a special-use label like \Sent. \All works on Gmail IMAP
cursorstringnextPageCursor or prevPageCursor from a previous response
pageintegerZero-indexed. IMAP accounts only
pageSizeintegerEntries per page, at most 100

search_messages

ArgumentTypeNotes
account (required)stringAccount id
search (required)objectSearch criteria: from, to, subject, body, since, before, seen, flagged, emailId, header and more. See Searching Messages
pathstringFolder to search. One folder at a time, not the whole account
cursor, page, pageSizePaging, as above
useOutlookSearchbooleanMS Graph only: use $search instead of $filter

get_message

ArgumentTypeNotes
account (required)stringAccount id
message (required)stringMessage id from a listing or search
markAsSeenbooleanSet \Seen while reading

The body comes back inline - see Message bodies for its shape and the rendering the tool pins.

get_message_text

ArgumentTypeNotes
account (required)stringAccount id
text (required)stringThe text.id value from get_message or a listing

get_attachment

ArgumentTypeNotes
account (required)stringAccount id
attachment (required)stringAttachment id from the attachments array of get_message

Returns the file inline as a base64 MCP resource. See Binary results for the size limit.

Organizing

update_message

ArgumentTypeNotes
account (required)stringAccount id
message (required)stringMessage id
flagsobject{ "add": ["\\Seen"], "delete": ["\\Flagged"], "set": [...] }
labelsobjectSame shape, Gmail only

move_message

ArgumentTypeNotes
account (required)stringAccount id
message (required)stringMessage id
path (required)stringDestination folder path
sourcestringSource folder path. Gmail API accounts only, where it is what removes the old label

The message id changes when a message moves. The response carries the new one.

delete_message

ArgumentTypeNotes
account (required)stringAccount id
message (required)stringMessage id
forcebooleanDelete outright instead of moving to Trash. Not supported on Gmail API accounts

Deleting moves the message to Trash when it is not already there, and deletes it permanently when it is.

Writing and sending

create_draft stores a message in a folder. Nothing is sent.

ArgumentTypeNotes
account (required)stringAccount id
path (required)stringTarget folder, usually the Drafts folder from list_mailboxes
from, to, cc, bccobject / arrayAddresses
subject, text, htmlstringContent
attachmentsarrayAttachment objects
referenceobjectDraft a reply or a forward, see below
flagsarrayFlags for the stored copy, for example ["\\Draft"]

send_message queues a message for delivery.

ArgumentTypeNotes
account (required)stringAccount id
to, cc, bccarrayRecipients
subject, text, htmlstringContent
from, replyToobjectSender addresses. from defaults to the account's own address
referenceobjectReply to or forward a stored message, see below
templatestringSend a stored template
renderobjectValues for the template's placeholders
attachmentsarrayAttachment objects
sendAtstringISO date to schedule the send

Delivery is queued, not immediate. The response carries a queueId, and the message shows up in the get_outbox listing until it is delivered.

Sending is the one irreversible tool

send_message reaches real recipients, and a queued message can only be cancelled while it is still in the outbox. Clients that honor MCP annotations treat it as an open-world call and ask for confirmation. If you would rather they could not call it at all, issue the token at the read-only access level - see Access Control.

Replying and forwarding

Both send_message and create_draft take a reference block, which is how an agent answers the message a user is looking at. Point it at a stored message and write only the new text: EmailEngine derives the subject, the recipients of a reply and the In-Reply-To/References headers from the referenced message, and flags that message as answered or forwarded once the new one is sent.

FieldTypeNotes
messagestringId of the message being answered or passed on
actionstringreply (default), reply-all or forward
inlinebooleanQuote the original under the new text, the way an email client does. Off by default
forwardAttachmentsbooleanCarry the original's attachments into the forwarded copy. Only meaningful with forward
ignoreMissingbooleanSend anyway if the referenced message cannot be found
messageIdstringVerify the original's Message-ID before sending
threadIdstringGmail thread to attach the outgoing message to
{
"name": "send_message",
"arguments": {
"account": "user123",
"reference": { "message": "AAAAAQAACnA", "action": "reply", "inline": true },
"text": "Thanks - Tuesday at 10:00 works for me."
}
}

Queue and templates

get_outbox takes page and pageSize (at most 100). It lists queued and scheduled messages with their delivery progress.

list_templates takes account (for account-specific templates; omit for shared ones), page and pageSize (at most 100).

Message bodies

Both body tools return exactly one rendering: sanitized web-safe HTML, generated from the plaintext part when a message carries no HTML one. There is no plaintext twin beside it, and the rendering is not the agent's to choose - textType, webSafeHtml, preProcessHtml, embedAttachedImages and maxBytes are all pinned by the server and absent from the tool schemas.

get_message carries the body inline, so reading one message is one call:

{
"subject": "Your ticket #8812 has been resolved",
"from": { "name": "Support", "address": "support@example.org" },
"flags": [],
"text": {
"id": "AAAAAQAAAAaTkaExkaEykA",
"encodedSize": { "plain": 175, "html": 219 },
"html": "<div style=\"overflow: auto;\"><p>We closed ticket #8812.</p>...</div>",
"hasMore": false,
"webSafe": true
}
}

hasMore: true means the body was longer than the budget below. That is when get_message_text earns its call, using text.id:

{
"html": "<div style=\"overflow: auto;\">...</div>",
"webSafe": true,
"hasMore": false
}

Quoted history is marked, not stripped. Reply and forward history is wrapped in a <details class="ee-collapsed-thread"> element - the boundary an email client hides behind a "show more" control. Everything outside it is what the sender wrote this time, which on a long thread is a small fraction of the bytes. The server instructions name the class, so a model can use it without being told. See Web Safe HTML.

Attached images are not inlined. An agent reads a body rather than displaying it, so cid: references are left as they are instead of being expanded into data URIs that would multiply the size of the result. The references name attachments get_attachment can fetch.

Results

A successful tool call returns the endpoint's JSON response twice: once as text, for models that read the content block, and once as structuredContent, for clients that parse it.

{
"jsonrpc": "2.0",
"id": 2,
"result": {
"content": [
{
"type": "text",
"text": "{\"total\":1,\"pages\":1,\"page\":0,\"accounts\":[{\"account\":\"user123\",\"name\":\"John Doe\",\"email\":\"john.doe@example.com\",\"type\":\"imap\",\"state\":\"connected\"}]}"
}
],
"structuredContent": {
"total": 1,
"pages": 1,
"page": 0,
"accounts": [
{
"account": "user123",
"name": "John Doe",
"email": "john.doe@example.com",
"type": "imap",
"state": "connected"
}
]
}
}
}

The JSON is compact on purpose: indentation is padding to a model, and it counts against the size cap below.

Errors

A failed tool call is a result, not a protocol error. The result carries isError: true and the API's own error body, so an agent can read what went wrong and correct itself:

{
"jsonrpc": "2.0",
"id": 3,
"result": {
"content": [
{
"type": "text",
"text": "{\"statusCode\":403,\"error\":\"Forbidden\",\"message\":\"Unauthorized permission\",\"requiredPermission\":{\"action\":\"send\",\"group\":\"submit\"}}"
}
],
"isError": true
}
}

Argument mistakes are caught before dispatch and named:

{ "content": [{ "type": "text", "text": "Missing required tool argument: message" }], "isError": true }
{ "content": [{ "type": "text", "text": "Unknown tool argument: bogus" }], "isError": true }

Calling a tool that does not exist is a JSON-RPC error rather than a result, because no tool ran - see Error codes.

Size limits

LimitValueWhat happens
Tool result128 KBThe text is cut at the limit and a notice is appended naming the full size; structuredContent is omitted so the cap is not defeated
get_message body32768 charactersThe body is cut before rendering and text.hasMore is set
get_message_text body65536 charactersSame, reported as hasMore
Page size on any listing100 entriesThe schema says so, and a larger request is clamped
Inline attachment1 MBget_attachment refuses and points at the REST download endpoint
Accounts in resources/list500Larger instances are browsed with list_accounts and its paging arguments

The two body budgets are input bounds: they cut each text part before the web-safe rendering runs, and that rendering can come out somewhat larger than what went in. The 128 KB result cap is the promise. They are set well under it so an ordinary message never reaches truncation, because a truncated result leaves the caller with a JSON fragment it cannot parse.

A single message can carry megabytes of text, and an oversized result degrades or breaks the calling model, so the caps err low. When one bites, narrow the request rather than working around it: page smaller, search instead of listing, and follow hasMore only when the rest of the body actually matters.

Binary results

get_attachment returns an embedded resource rather than text:

{
"content": [
{
"type": "resource",
"resource": {
"uri": "emailengine://account/user123/attachment/AAAAAQAACnAcdefgh",
"mimeType": "application/pdf",
"blob": "JVBERi0xLjQKJcfs..."
}
}
]
}

The URI is a stable identifier for the client to attach the blob to. It is not listed by resources/list and not readable with resources/read - the content is in the result.

Resources

Each account the credential can see is published as an MCP resource:

{
"uri": "emailengine://account/user123",
"name": "user123",
"title": "John Doe",
"description": "john.doe@example.com, state: connected",
"mimeType": "application/json"
}

resources/read on that URI returns the same payload as get_account. Clients that browse resources can therefore show what a credential reaches without calling a tool, and an account-bound credential sees exactly its own account.

Accounts whose id contains /, ? or # are skipped in the listing: they cannot round-trip through the URI. Their mail is still reachable through the tools, which take the id as an argument.

Modern-revision clients can also subscribe to an account resource and be notified when its state changes - see Subscriptions.

See Also