mail-mcp-server
Syncs a Gmail mailbox over IMAP using an app password, downloads messages locally, converts bodies and attachments to Markdown, indexes them, and lets the agent search and read mail and attachments without modifying the mailbox.
Syncs a Mail.ru mailbox over IMAP, builds a local searchable copy of messages and attachments, and provides read-only search, message reading, folder listing, and attachment access tools.
Mail MCP Server
Give your AI agent your whole mailbox, including on-premises Exchange, with no way to change it.
MCP server (stdio) for on-premises Microsoft Exchange over EWS and any IMAP mailbox with password sign-in, tested on Mail.ru and Gmail. It downloads every message once, converts bodies and attachments to Markdown on your machine, builds a Lucene search index, and answers the agent from that copy, so text inside PDF, Office and ZIP attachments is searchable in Russian and English.
Status: version 0.1.0 is published. Checked end to end on Exchange 2013 (about 8
thousand messages), Mail.ru (about 34 thousand) and Gmail with an app password. Developed and
tested on Windows.
Contents: Features · How it works · Typical Scenarios · Quickstart · MCP Tools · Commands · Build · Stack · Documentation · License
Features
Works with on-premises Exchange. EWS with NTLM and your normal domain password, tested on Exchange 2013. No Microsoft Graph needed. Other mailboxes work over IMAP, tested on Mail.ru and Gmail.
Attachments are searchable too. Text from PDF, Word, Excel, PowerPoint, ZIP archives, attached emails, Outlook
.msgandwinmail.datis extracted and indexed, so the agent can find the contract inside the attachment, not just the message that carried it.Search that understands word forms. Russian and English, across subject, body, addresses and attachments. Copies of one message appear once, and conversations are followed across folders (search).
Cannot change your mailbox. Read-only is enforced at the protocol level: write commands are rejected before they are sent, and downloading a message does not mark it as read (security).
Careful with your account. One sign-in per sync and no retries. After a rejected password, it stops trying until you fix it, which reduces the risk of a domain lockout.
Fast, and works offline. The agent reads a local copy, so answers don't depend on the mail server. Sync runs only when you ask for it.
Your mail stays on your machine. No cloud service of its own: conversion and indexing are local, and the local copy is unfiltered. Your AI client still sees what the tools return, and an optional secret filter masks passwords and codes in responses.
Several mailboxes, one server. Passwords can stay out of the config, in Windows Credential Manager, a password manager or an environment variable (connections).
Related MCP server: work-assistant
How it works
Download all mail. The
synctool copies the included folders of each mailbox as original.emlfiles, then only changes on later runs.Convert it locally to Markdown. Bodies and attachments — PDF, Office documents, archives, attached messages — are converted with pandoc (required), PDFBox and Apache POI.
Index it. A Lucene index per mailbox with Russian and English word forms.
Use it from MCP. Tools search and read the local copy only; they never contact a mail server.
MCP client (Claude Code, Codex, OpenCode, VS Code, ...)
| JSON-RPC over stdin / stdout
v
mail-mcp-server --- search, read ---> Lucene index + <data-dir>/<connection>/messages/
| ^
+--- sync (on request) --- EWS / IMAP --+--> .eml -> Markdown -> indexTypical Scenarios
The agent picks the tools itself; these are the chains it usually follows.
Scenario | Example request | Tools |
Find mail on a topic | "Find the letters about the supply contract in |
|
Mail of a period | "What came to my work mail yesterday?" |
|
Follow a conversation | "Show the whole discussion around this letter." |
|
Read an attachment | "What does the price list in the last letter from the supplier say?" |
|
Bring mail up to date | "Update my work mail and tell me what is new." |
|
Browse a folder | "List the latest messages in my inbox." |
|
Quickstart
1. Install JDK 25 and pandoc 3.x. Pandoc is required
and must be on PATH. Check in a new terminal:
java -version
pandoc --version2. Get the jar: download mail-mcp-server.jar from the
latest release, or build it yourself:
.\gradlew.bat bootJar # -> build\libs\mail-mcp-server.jar3. Describe your mailboxes in %USERPROFILE%\.mail-mcp-server\connections.json
(~/.mail-mcp-server/connections.json elsewhere), one entry per mailbox. Keep only the ones you
need and replace the example addresses:
{
"connections": {
"work": {
"type": "ews",
"url": "https://webmail.example.com/EWS/Exchange.asmx",
"auth": "ntlm",
"username": "user",
"windowsCredential": "mail-mcp/work",
"description": "Work mail, Exchange 2013"
},
"mailru": {
"type": "imap",
"host": "imap.mail.ru",
"port": 993,
"tls": true,
"auth": "app-password",
"username": "user@mail.ru",
"passwordCommand": ["security", "find-generic-password", "-s", "mail-mcp", "-a", "mailru", "-w"],
"description": "Personal Mail.ru"
},
"gmail": {
"type": "imap",
"host": "imap.gmail.com",
"port": 993,
"tls": true,
"auth": "app-password",
"username": "user@gmail.com",
"password": "${MAIL_GMAIL_PASSWORD}",
"description": "Personal Gmail"
}
}
}Each entry shows a different password source: work reads Windows Credential Manager, mailru
runs a command that prints the password (here the macOS Keychain; 1Password, pass and others
work the same way), and gmail reads an environment variable, which must be set wherever the
server starts. A literal password works too. Which password each provider needs (domain password, Mail.ru or Gmail
app password, Gmail OAuth2) and how to store it: docs/connections.md.
4. Register the server with your MCP client, with an absolute path to the jar. Claude Code:
claude mcp add --scope user mail -- java -jar C:/path/to/mail-mcp-server.jarOther clients and environment variables: docs/clients.md. If you installed
pandoc or changed PATH while the client was running, fully restart the client.
5. Download your mail. Ask the agent: "Sync my work mailbox and tell me when it's done."
The first sync downloads the whole mailbox and can take a long time, so keep the client open. If it
stops, ask again and it continues. A password manager must be unlocked or ask in its own window,
because the server has no terminal to ask in. For a large mailbox, or to see progress in a
terminal, use mail-sync.
6. Try it. Ask the agent: "Which mailboxes can you read?" or "Find the letters about the
supply contract in work from last week."
Something went wrong: docs/troubleshooting.md.
MCP Tools
Every tool except listConnections takes a required connection argument.
Tool | What it does |
| Configured mailboxes with type, sign-in method, description and configuration errors |
| Starts a background synchronization and returns at once |
| Progress and result of the current or last run |
| Local folders with message counts |
| Message cards of one folder, newest first, in pages |
| Headers, the body in chunks, attachments with file paths |
| The text of one attachment in chunks, and the file path |
| Full-text search with Russian and English word forms, fields, folder and date filters |
| The conversation of a message across folders, oldest first |
Query syntax and results: docs/search.md. Arguments and limits: docs/tools-agent.md.
Commands
Offline maintenance, sign-in recovery and Gmail OAuth2 consent run in a terminal: docs/commands.md.
Build
.\gradlew.bat build # compile, all tests, bootJar -> build\libs\mail-mcp-server.jarThe build needs a JDK 25 toolchain; Gradle can download one. Tests never contact a mail server:
they use loopback fakes and the synthetic data sets in testdata/. Tests that need pandoc are
skipped when it is not installed.
Stack
Java 25, Gradle 9 with a version catalog
Spring Boot
4.2.0-RC1, Spring AI2.1.0-RC1MCP server (stdio)Apache HttpClient 5.6.5 (EWS with NTLM), StAX for SOAP
Eclipse Angus Mail 2.0.5 (IMAP and MIME parsing)
Apache Lucene 10.5.2 (index, Russian and English analysis, highlighting)
pandoc 3.x (external process), Apache PDFBox 3.0.5, Apache POI 5.4.1
Documentation
Document | Contents |
The connections file, passwords, Exchange, Mail.ru and Gmail setup | |
Registering the server with MCP clients, environment variables | |
User-run commands, their output and exit codes | |
Search syntax, dates, copies, conversations, word forms | |
Read-only protection, sign-in policy, credentials, local data, secret filter | |
Common problems and what to check first | |
Rules for AI agents changing this repository, and the map of engineering documents ( | |
Plan, decisions and real-mailbox results |
License
This project is licensed under the Apache License, Version 2.0. Dependencies keep their own licenses: THIRD_PARTY_NOTICES.md.
Available Tools
9 toolsgetAttachmentgetAttachmentARead-onlyIdempotent
Read an attachment's saved extracted text as Markdown, in chunks, with its metadata and local file paths. This reads existing text; it does not convert the file on demand. If text is absent, extracted text is unavailable, not proof that the original attachment is empty. The original may still be available at path; path is absent if the file is missing.
Get file from getMessage's attachments[].file or a search hit's snippetAttachmentFile, keeping that message's connection and key. To continue a long text, call this tool again with the same connection, key and file and offset=nextOffset. Stop when there is no nextOffset.
| Name | Required | Description | Default |
|---|---|---|---|
| key | Yes | Local key of the message containing this attachment, from getMessage or the search hit; use the same connection | |
| file | Yes | Exact attachments[].file from getMessage or snippetAttachmentFile from search for this message. Use this value unchanged, not name, path or textPath | |
| offset | No | Text character offset, default 0. Use nextOffset from this tool to continue this attachment, not an offset from message-body or search results | |
| maxChars | No | Text characters per chunk: 1 to 100000, default 20000; larger values are capped at 100000 | |
| connection | Yes | Connection name from listConnections |
Output Schema
| Name | Required | Description |
|---|---|---|
| key | Yes | Message key. |
| file | Yes | Attachment file, as requested. |
| name | No | Original file name. |
| path | No | Absolute path of the original file; absent when it is not saved. |
| size | Yes | Size in bytes. |
| text | No | Chunk of the saved extracted text as Markdown; [redacted:<kind>] marks a hidden secret. Absent when extracted text is unavailable; the original attachment may still have content and be available at path. |
| offset | Yes | Character position of this chunk. |
| textPath | No | Absolute path of the extracted Markdown text; absent when there is no text. |
| nextOffset | No | Pass as offset to get the next chunk; absent at the end. |
| textLength | Yes | Length of the whole text in characters; 0 without text. |
| contentType | Yes | MIME type, e.g. application/pdf. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Beyond the read-only and idempotent annotations, it discloses important behavior: chunked reads, metadata and local paths, continuation rules, and the distinction between unavailable extracted text and an empty original attachment. These are meaningful operational details that help the agent avoid misinterpreting absence.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is front-loaded with purpose and then gives usage and caveats in short, purposeful sentences. There is little redundancy, and the continuation guidance is placed where an agent needs it.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
The tool is moderately complex with a full schema, complete schema descriptions, annotations, and an output schema. The description fills the remaining gaps around chunked reading, continuation, missing text, and missing files without needing to repeat return-value details.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is complete, so the schema already documents all parameters. The description still adds useful cross-parameter semantics by explaining where file, key, and connection come from and that continuation requires the same connection, key, and file with offset=nextOffset.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description states a specific verb and resource: reading an attachment's saved extracted text as Markdown in chunks. It also explicitly distinguishes this from on-demand conversion, so the agent understands the tool reads existing extraction rather than creating it.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
It gives clear sourcing guidance: obtain file from getMessage attachments or a search hit's snippetAttachmentFile, and reuse the same connection and key. It also explains continuation via offset=nextOffset and when to stop, though it does not name a specific alternative tool for on-demand conversion.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
getMessagegetMessageARead-onlyIdempotent
Read one locally stored message: headers, a body chunk as Markdown, and an attachment list. nextOffset continues only the body; headers and the attachment list are returned on each call. If body is absent, saved body text is unavailable; inspect state and error rather than assuming the original message was empty.
To read an attachment, call getAttachment with the same connection and key, and file=attachments[].file. Use file, not name or path. The list includes files extracted from attached archives and messages; parent is the containing attachment's file. For the conversation, call getThread with the same connection and key.
| Name | Required | Description | Default |
|---|---|---|---|
| key | Yes | Local message key from listMessages, search or getThread in this connection; use key, not the Message-ID header | |
| offset | No | Body character offset, default 0. To continue, keep the same connection and key and use nextOffset from this tool. Stop when there is no nextOffset | |
| maxChars | No | Body characters per chunk: 1 to 100000, default 20000; larger values are capped at 100000 | |
| connection | Yes | Connection name from listConnections |
Output Schema
| Name | Required | Description |
|---|---|---|
| cc | No | Cc recipients as Name <address>. |
| to | No | To recipients as Name <address>. |
| key | Yes | Message key. |
| body | No | Chunk of the saved body as Markdown; [redacted:<kind>] marks a hidden secret. Absent when saved body text is unavailable; this does not prove the original body was empty. |
| date | No | Date from the message header; absent when missing. |
| from | No | Senders as Name <address>. |
| read | No | Read in the mailbox as of the last sync; absent when unknown. |
| error | No | Why the message failed; present when state is failed. |
| state | Yes | converted (complete), downloaded (saved, text not processed yet) or failed (see error). |
| folder | Yes | Folder path in the mailbox; not the folder key used by listMessages/search. |
| labels | No | Mailbox labels of this message as of the last sync, such as Gmail's Inbox, Sent or user labels; empty when it has none. Absent when unknown or when the mailbox has no labels. |
| offset | Yes | Character position of this chunk. |
| flagged | No | Flagged or starred in the mailbox as of the last sync (not sender importance); absent when unknown. |
| subject | No | Subject. |
| inReplyTo | No | In-Reply-To header. |
| localPath | Yes | Absolute message directory, readable directly: message.eml (original), body.md, attachments/. |
| messageId | No | Message-ID header. |
| bodyLength | Yes | Length of the whole body in characters; 0 without a body. |
| nextOffset | No | Pass as offset to get the next chunk; absent at the end. |
| references | No | References header message IDs. |
| attachments | Yes | Attachments, including files unpacked from attached archives and messages. Empty when none are found or the saved MIME cannot be read. Pass an entry's file to getAttachment with this message's key and connection. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations cover the safety profile (readOnly, idempotent, non-destructive), and the description adds behavior the annotations can't: pagination continues only the body while headers and the attachment list are re-returned each call, and an absent body means saved body text is unavailable rather than the original being empty. That last point is a genuine anti-misinterpretation warning.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Two tight paragraphs with the return shape front-loaded and the escape hatches second. Minor redundancy with the offset/nextOffset schema text, but nothing is padding.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Output schema exists so return values need no prose, and the description still covers the edge cases an agent would otherwise get wrong: body-only pagination, empty-body interpretation, archive-extracted attachments and the parent field. Nothing needed to call it correctly is missing.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100%, so key, offset, maxChars and connection are already documented, including the nextOffset continuation rule. The description restates that rule and adds only the `file` vs `name`/`path` disambiguation, which applies to the sibling tool rather than this one's parameters.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
Opens with a specific verb and resource plus the exact payload ('headers, a body chunk as Markdown, and an attachment list'), which no sibling tool claims. It also implicitly separates itself from getThread and getAttachment by naming them for those respective jobs.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Explicitly routes adjacent needs: attachments go to getAttachment with the same connection/key and file=attachments[].file, the conversation goes to getThread. It also gives the continuation rule (keep connection and key, follow nextOffset, stop when absent), so an agent knows when to call again versus call elsewhere.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
getThreadgetThreadARead-onlyIdempotent
List the locally indexed conversation containing a message, across all folders of one connection. Returns cards without bodies, oldest first, with undated messages last. Only locally indexed messages can appear. Messages are linked by reply headers (Message-ID, In-Reply-To, References), not by subject or participants.
Read each message with getMessage using the same connection and that card's key. Recognized copies are returned once, with other keys and folders under copies; total and paging count groups.
| Name | Required | Description | Default |
|---|---|---|---|
| key | Yes | Local message key from listMessages or search in this connection. Any member of the conversation works; use key, not the Message-ID header | |
| limit | No | Results per page: 1 to 200, default 50; larger values are capped at 200 | |
| offset | No | Default 0. For the next page, keep the same connection and original key and use the previous nextOffset. Stop when there is no nextOffset | |
| connection | Yes | Connection name from listConnections |
Output Schema
| Name | Required | Description |
|---|---|---|
| key | Yes | Message key the conversation was requested for. |
| total | Yes | Messages in the conversation; copies of one message count once. |
| offset | Yes | Position of this page. |
| messages | Yes | Messages, oldest first; undated last. |
| connection | Yes | Connection name. |
| nextOffset | No | Pass as offset to get the next page; absent at the end. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare read-only, idempotent, non-destructive, but the description adds substantial substance beyond them: cards come back without bodies, ordered oldest first with undated last, only locally indexed messages appear, messages are linked by Message-ID/In-Reply-To/References rather than subject or participants, and recognized duplicates are collapsed into a copies field with totals counting groups.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Two tight paragraphs, front-loaded with the scope and ordering before the follow-up instructions. Nearly every sentence carries load, though the trailing clause 'total and paging count groups' is terse enough to be slightly opaque.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
An output schema exists, so return shape needn't be spelled out, yet the description still covers the semantics an agent needs: body-less cards, ordering, duplicate collapsing, and header-based threading. Combined with annotations covering the safety profile, nothing needed to call it correctly is absent.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100% and already documents key, limit range and cap, and the offset/nextOffset continuation protocol, so the baseline applies. The description reinforces that the 'key' is a card key from the returned list rather than a Message-ID header, but adds no new syntax or constraints beyond the schema.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb and resource — 'List the locally indexed conversation containing a message, across all folders of one connection' — which cannot be confused with listMessages or search. It also distinguishes itself from getMessage by positioning that sibling as the follow-up for reading bodies.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Explicitly routes the agent: use this to enumerate the conversation, then call getMessage with the same connection and each card's key. It gives a paging stopping rule ('Stop when there is no nextOffset'). What's missing is an explicit statement of when NOT to use it (e.g., a single-message read or a subject-based grouping), though the linking-by-headers note implies the latter.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
listConnectionslistConnectionsARead-onlyIdempotent
List the configured mailboxes. Call first when the mailbox to use is not known yet: name is the value for the 'connection' argument of every other tool, description says what the mailbox is for, configError explains why an entry cannot be used. This reads local configuration without signing in. localDataAvailable indicates a local data directory, not complete or up-to-date mail; use listFolders and syncStatus to inspect coverage.
| Name | Required | Description | Default |
|---|---|---|---|
No parameters | |||
Output Schema
| Name | Required | Description |
|---|---|---|
| connections | Yes | Configured mailboxes; empty when none is configured. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
The description adds meaningful behavior beyond the annotations: it reads local configuration without signing in, and it warns that localDataAvailable does not imply complete or up-to-date mail. Those details help an agent interpret and trust the result correctly.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The purpose is front-loaded in the first sentence, followed by usage and field semantics in a compact sequence. Every sentence carries actionable information, with no redundant restatement of the name or title.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a zero-parameter read-only listing tool with an output schema and annotations covering safety, the description is complete enough. It explains when to call it, how to use its output, and an important caveat about local data coverage.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
There are no input parameters, so parameter semantics are inherently minimal and the schema has nothing to document. The description instead explains key output field meanings, which is useful but outside the parameter dimension.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description states a specific verb and resource: 'List the configured mailboxes.' It also distinguishes the tool from siblings by explaining that it should be called first when the mailbox is unknown and that its name field feeds the 'connection' argument of every other tool.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
It gives an explicit trigger: 'Call first when the mailbox to use is not known yet.' It also redirects agents to listFolders and syncStatus for coverage inspection, which clarifies an important alternative path.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
listFolderslistFoldersARead-onlyIdempotent
List locally known folders in one connection, with folder keys, paths and local indexed message counts. Pass a folder's key as folder to listMessages or search; keep the same connection. synced=false means a completed folder sync has not been recorded and messages may be missing. synced=true does not guarantee current mail; use syncStatus for the latest sync's time and outcome. This tool does not discover folders on the server.
| Name | Required | Description | Default |
|---|---|---|---|
| connection | Yes | Connection name from listConnections |
Output Schema
| Name | Required | Description |
|---|---|---|
| folders | Yes | Folders. |
| connection | Yes | Connection name. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnly/idempotent/non-destructive, and the description goes well beyond them by disclosing the meaning of synced=false (a completed folder sync not recorded, messages may be missing) and the subtle caveat that synced=true does not guarantee current mail. That is exactly the non-obvious data-freshness behavior an agent needs before trusting results.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Five short sentences, front-loaded with what the tool returns, then downstream usage, then freshness caveats, then negative scope. No sentence is redundant with another or with the structured fields.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
An output schema exists so return shape needn't be described; the description instead fills in the interpretive context the schema cannot carry (sync semantics, cross-tool key usage, non-discovery). Nothing an agent needs to call this correctly is missing.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The single parameter has 100% schema coverage and already points to listConnections, so baseline would be 3. The description adds real meaning by stressing one connection and 'keep the same connection', signalling that folder keys are scoped to a connection and misusing this yields wrong lookups.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb and resource (list locally known folders) plus exactly what is returned (folder keys, paths, local indexed message counts). It also scopes itself negatively against a hypothetical discovery tool ("does not discover folders on the server") and routes the folder key to listMessages/search, so an agent can place it among siblings without opening the schema.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Explicitly tells the agent what to do with the output ("Pass a folder's key as folder to listMessages or search; keep the same connection") and what to consult when freshness matters (syncStatus). It lacks an explicit when-not-to-call statement, though the non-discovery scoping sentence covers most of that gap.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
listMessageslistMessagesARead-onlyIdempotent
Browse locally indexed messages in one folder, newest first, optionally within a date range. Undated messages come last without date bounds and are excluded with bounds. For keyword search use search; for a date range across all folders use search without query.
Returns cards without bodies. Pass a card's key and the same connection to getMessage to read it, or to getThread for its conversation. Every stored copy counts separately here; search and getThread group recognized copies. read/flagged describe the last sync observation.
| Name | Required | Description | Default |
|---|---|---|---|
| limit | No | Results per page: 1 to 200, default 50; larger values are capped at 200 | |
| since | No | Include messages at or after this date/time, using the message Date header, not receipt or download time. Format: YYYY-MM-DD (UTC midnight), or ISO-8601 with Z or an offset, e.g. 2026-10-01T09:00:00+03:00; at most 3 fractional second digits. Messages without a date are excluded | |
| until | No | Include messages strictly before this date/time; same header and format as since. To include a whole day, use the following day's midnight. Must be later than since when both are given. Messages without a date are excluded | |
| folder | Yes | One folder key from listFolders in this connection, not its display name | |
| offset | No | Default 0. For the next page, repeat the same arguments and use the previous nextOffset. Stop when there is no nextOffset | |
| connection | Yes | Connection name from listConnections |
Output Schema
| Name | Required | Description |
|---|---|---|
| since | No | Applied since bound, in UTC. |
| total | Yes | Messages in the folder within the date bounds. |
| until | No | Applied until bound, in UTC. |
| folder | Yes | Folder key. |
| offset | Yes | Position of this page. |
| messages | Yes | Messages, newest first; undated last. |
| connection | Yes | Connection name. |
| nextOffset | No | Pass as offset to get the next page; absent at the end. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnly/idempotent/non-destructive, and the description adds substantial behavior beyond them: undated messages sort last without bounds but are excluded with bounds, results are cards without bodies, every stored copy is counted separately whereas search and getThread group recognized copies, and read/flagged reflect the last sync observation. These are non-obvious semantics that materially change how results should be interpreted.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Four short sentences, front-loaded with what the tool returns and how it differs from search, then the follow-up path, then the subtle copy-counting nuance. No filler and no repetition of schema field descriptions.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
An output schema exists, yet the description still sketches the card shape and the read/flagged provenance, which is the right level. Between the description, the 100%-covered schema, and the annotations, an agent has everything needed to call it correctly and chain into getMessage/getThread.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100%, so the baseline is 3; the description earns extra credit by adding cross-parameter behavior the schema does not state, namely the ordering and exclusion rule for undated messages at the since/until boundaries. It stops short of restating formats, which is appropriate since the schema already carries them.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb and resource (browse locally indexed messages), the scope (one folder), the ordering (newest first), and the optional date filter. It also explicitly distinguishes itself from 'search' and from the card-reading tools getMessage/getThread, so an agent can route without opening sibling schemas.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Gives explicit when-to-use-elsewhere rules: 'For keyword search use search; for a date range across all folders use search without query.' It also names the follow-up path (pass a card's key plus the same connection to getMessage, or to getThread), which is exactly the alternative-selection guidance an agent needs.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
searchsearchARead-onlyIdempotent
Search locally synced messages in one connection. Choose query and field for text search; optionally restrict by folder and dates. Text results are most relevant first. To list messages by date alone, omit query and supply since or until; results are newest first, without snippets.
Subject, body and attachment text support Russian and English word forms; irregular forms and synonyms may be missed. If results are sparse, try fewer words, other forms, synonyms or the other language in separate calls. An empty result does not prove the mail is absent: only synced, indexed content is searched.
Read a hit with getMessage using the same connection and the hit's key. Snippets mark matches as word, but may show only part of a match or body context. If a hit has snippetAttachmentFile, read that attachment with getAttachment using the same connection, the hit's key and file=snippetAttachmentFile. Recognized copies are returned once; copies lists other matching keys and folders. total and paging count these groups.
| Name | Required | Description | Default |
|---|---|---|---|
| field | No | Choose one: any (default; all fields below), subject, body, from, to (includes Cc), attachmentName (whole name or parts), attachmentText (extracted contents), label (mailbox label words, such as Gmail's Inbox, Sent or user labels; connections without labels match nothing). from/to search addresses and names. For an exact address match, use an unquoted full address: field=from, query=user@example.com. This ignores case and display names. any also searches content, so an address there need not be a participant. Without query, field has no effect | |
| limit | No | Results per page: 1 to 100, default 20; larger values are capped at 100 | |
| query | No | Search terms, a quoted phrase or a full email address; at most 1000 characters. Words combine with AND (common stop words may be ignored); double quotes make a phrase. No OR/NOT, wildcards, fuzzy matching or field-prefix syntax such as from:. Search alternatives in separate calls. Omit only when since or until is given | |
| since | No | Include messages at or after this date/time, using the message Date header, not receipt or download time. Format: YYYY-MM-DD (UTC midnight), or ISO-8601 with Z or an offset, e.g. 2026-10-01T09:00:00+03:00; at most 3 fractional second digits. Messages without a date are excluded | |
| until | No | Include messages strictly before this date/time; same header and format as since. To include a whole day, use the following day's midnight. Must be later than since when both are given. Messages without a date are excluded | |
| folder | No | One folder key from listFolders, not its display name. Omit to search all folders in the connection | |
| offset | No | Default 0. For the next page, repeat the same search arguments and use the previous nextOffset. Stop when there is no nextOffset | |
| connection | Yes | Connection name from listConnections |
Output Schema
| Name | Required | Description |
|---|---|---|
| hits | Yes | Hits, most relevant first; newest first for date-only search. |
| field | Yes | Field searched. |
| query | No | Text query; absent for date-only search. |
| since | No | Applied since bound, in UTC. |
| total | Yes | Matching messages; copies of one message count once. |
| until | No | Applied until bound, in UTC. |
| folder | No | Folder key searched; absent for all folders. |
| offset | Yes | Position of this page. |
| connection | Yes | Connection name. |
| nextOffset | No | Pass as offset to get the next page; absent at the end. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint, idempotentHint, openWorldHint and destructiveHint=false, so safety semantics are covered. The description nevertheless adds real substance beyond them: the corpus is limited to synced and indexed content, an empty result does not prove absence, morphology limits for Russian/English, ranking direction per mode, and deduplicated "recognized copies" counted in total and paging. Not a 4-5 because it does not quantify index freshness or the sync dependency beyond a passing mention.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Purpose is front-loaded, followed by search semantics, then how to read a hit, then result-shape notes; every paragraph has a job. It is dense but slightly over-packed — the getMessage/getAttachment sentences repeat 'using the same connection' twice and could be tightened.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
With 8 parameters, an output schema and rich annotations, this covers what remains: query syntax and its limits, date-header semantics, the two result orderings, snippet/copy structure, and the cross-tool hop needed to fetch an attachment. An agent can call it correctly and interpret the response without further guessing.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100%, so the schema carries the per-parameter detail and the description correctly does not repeat it. What it adds is search-quality semantics for query (word forms, synonyms, no field-prefix syntax) and a concrete paging procedure, which meaningfully explains how to drive the parameters rather than restating their types.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
Opens with a specific verb and resource plus scope: "Search locally synced messages in one connection." It immediately delineates two modes (text query vs date-only listing) and routes the agent to getMessage/getAttachment for follow-up, which distinguishes it from listMessages/listFolders without needing their schemas.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Explicit when-to-use rules: supply query for text search, omit it when only since/until are given, and try fewer words or the other language if results are sparse. It also names the alternative reading tools (getMessage, getAttachment) and the exact conditions and arguments that select them.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
syncsyncARead-only
Refresh one connection's local mail copy from the mail server, including new messages, removals, moves and read/flag states. The remote mailbox is not changed. Call when the user needs newer mail than the local copy. Runs in the background; the first sync can take a long time.
status=started means launched, not finished. For started or alreadyRunning, check syncStatus with the same connection while state is running; do not call sync again to check progress. lockedByOtherProcess means another process holds the local write lock and this call started no sync. That process may be an offline command; syncStatus reports sync runs only.
For refused, relay message to the user and do not retry: after a rejected sign-in, the user must fix the credentials and run mail-sync in a terminal.
| Name | Required | Description | Default |
|---|---|---|---|
| connection | Yes | Connection name from listConnections |
Output Schema
| Name | Required | Description |
|---|---|---|
| status | Yes | started (launched, not completed); alreadyRunning (a sync is already running in this process); lockedByOtherProcess (another process holds the local write lock, possibly for an offline command; this call started no sync); refused (see message). Use syncStatus for started/alreadyRunning; it reports sync runs, not offline command progress. |
| message | Yes | Explanation; relay it to the user when status is refused. |
| connection | Yes | Connection name. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Goes well beyond the annotations by documenting the async state machine: status=started means launched not finished, lockedByOtherProcess means no sync was started and may be an offline command, and refused requires user action in a terminal. The 'remote mailbox is not changed' clause reconciles with readOnlyHint=true and clarifies that only the local copy is written.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Front-loaded with purpose and scope in the first sentence, then organized into usage and state-handling sections with no filler. The dense state paragraph is justified by the tool's async behavior, though it could be tightened slightly for scanning.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
An output schema exists, so return values need no explanation, and the description still supplies the operational envelope: latency warning ('the first sync can take a long time'), background execution, per-status guidance, and a lock-conflict case. Nothing critical for correct invocation is missing.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100% and the single parameter is already documented as 'Connection name from listConnections'. The description only refers to 'the same connection' when describing syncStatus polling, adding no new format or constraint information. Baseline 3 is appropriate.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb and resource ('Refresh one connection's local mail copy from the mail server') and enumerates exactly what is propagated (new messages, removals, moves, read/flag states), plus an explicit non-effect ('The remote mailbox is not changed'). This clearly separates it from syncStatus, which only reports progress.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Gives an explicit trigger ('Call when the user needs newer mail than the local copy'), an explicit anti-pattern ('do not call sync again to check progress' → use syncStatus with the same connection), and a failure-path instruction ('For refused, relay message to the user and do not retry'). No inference required.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
syncStatussyncStatusARead-onlyIdempotent
Read local progress or the result of the current or last sync; this does not start a sync or contact the mail server. state=running means still working; completed means the run finished; failed means the run stopped with an error; interrupted means the syncing process ended before finishing; never means no sync is recorded. phase is the stage reached; checkingLocal and checkingFolders can take minutes with no folder progress yet.
While running, other tools may see incomplete results. Check state to decide whether to keep waiting. The failed count is a count of messages: completed can still have failed>0. Inspect error and signInRejected for a failed run. If signInRejected is true, tell the user to fix credentials and run mail-sync in a terminal; repeated sync calls cannot recover it. Timestamps describe this run, not a guarantee that the local mail is current now.
| Name | Required | Description | Default |
|---|---|---|---|
| connection | Yes | Connection name from listConnections |
Output Schema
| Name | Required | Description |
|---|---|---|
| error | No | Why the sync failed. |
| phase | No | Stage reached, in order: checkingLocal (comparing saved messages with the index, offline; long after a run that did not complete), signingIn, checkingFolders (collecting changes from every folder), applyingChanges (moves and flag changes), downloading (removing and downloading folder by folder). For failed or interrupted, the stage where the run stopped. Absent for never and for runs recorded by an older version. |
| state | Yes | running (still working), completed (run finished, possibly with individual message failures), failed (run stopped with an error), interrupted (syncing process ended before finishing), or never (no recorded sync). |
| failed | Yes | Count of new messages that failed to download or convert, all folders. May be nonzero even when state is completed. |
| folders | Yes | Progress per folder. |
| removed | Yes | Local messages removed because they are gone from the mailbox, all folders. |
| startedAt | No | When the sync started. |
| connection | Yes | Connection name. |
| downloaded | Yes | New messages saved, all folders. |
| finishedAt | No | When the sync ended; absent while running. |
| signInRejected | Yes | True when the mailbox rejected the sign-in; sync works again only after the user fixes the credentials and runs mail-sync in a terminal. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Goes well beyond the readOnly/idempotent annotations: enumerates every state and phase value, warns that checkingLocal/checkingFolders can stall for minutes with no progress, notes other tools may see incomplete results while running, clarifies that completed can still have failed>0 (count is of messages, not runs), and explains the signInRejected recovery path. No contradiction with annotations — non-destructive read is consistent.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Dense but front-loaded: the core read-only claim and the state/phase glossary lead, with edge-case handling following. Some sentences (timestamps caveat, phase latency note) are closely packed, but each conveys distinct operational information rather than repeating the schema.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Output schema exists, yet the description still explains the semantics of the key return fields (state, phase, failed, error, signInRejected) that an agent needs in order to act. Combined with the latency and recovery guidance, nothing necessary for correct invocation or follow-up behavior is missing.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Only one parameter (`connection`) and schema coverage is 100%, with the schema itself pointing at listConnections for valid values. The description adds no further parameter semantics, which is acceptable here since the schema fully covers it, but there is no value added beyond it.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
Specific verb+resource: reads progress/result of the current or last sync, and explicitly disclaims what it does not do (start a sync, contact the mail server). That negative clause is exactly what separates it from the sibling `sync` tool without opening either schema.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Gives clear operational context: 'Check state to decide whether to keep waiting', and warns that repeated sync calls cannot recover a signInRejected failure — the agent should instead tell the user to fix credentials in a terminal. It stops short of explicitly naming `sync` as the tool that actually starts a sync, leaving that inference to the reader.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
9 tool updates
v0.1.0- First observed
getAttachment - First observed
getMessage - First observed
getThread - First observed
listConnections - First observed
listFolders - First observed
listMessages - First observed
search - First observed
sync - First observed
syncStatus
TDQS
Scored across 9 tools
Each tool has a clearly distinct role: connection discovery, folder browsing, message browsing/reading, search, thread reading, attachment reading, sync, and sync status. The descriptions explicitly disambiguate close pairs like listMessages vs. search and getMessage vs. getThread.
Most tools follow a consistent camelCase verb_noun pattern such as listConnections, getMessage, listFolders, and getThread. The bare verbs search and sync plus syncStatus are minor deviations but still readable and predictable.
Nine tools is well-scoped for a local mail access server. Each tool earns its place by covering a specific part of the browse/read/search/sync workflow.
The set covers the core read-only mail workflow: connections, folders, messages, search, threads, attachments, syncing, and sync status. It lacks compose/send or message mutation operations, but those may be outside the apparent local-copy purpose.
Maintenance
Related MCP Connectors
Your own AI reads, searches and drafts in your mailbox, on your Windows computer.
Email for AI agents: read, search and send from your Gmail, Outlook, Yahoo, iCloud or IMAP inbox.
- Lettio MCPOAutheu.lettio
Private, EU-hosted email for AI agents over JMAP: read, search, reply, organize, send.
Your IMAP mailbox as an MCP server: read, search and (if you allow it) organize mail. Open source.
Related MCP Servers
- AlicenseAqualityBmaintenanceReads and searches local email archives (.mbox or .eml) with full MIME parsing, enabling queries about senders, subjects, dates, and bodies while keeping all data on your machine.3188 npmMIT
- AlicenseNot gradedqualityBmaintenanceEnables AI agents to search and read a local, provider-independent email archive, reconstruct contacts and interactions, and prepare draft responses without sending anything.1MIT
- AlicenseNot gradedqualityAmaintenanceEnables AI assistants to read, search, send, reply, forward, schedule, and organize email across multiple IMAP/SMTP accounts, including drafts, labels, bulk operations, real-time mailbox watching, verification-code capture, calendar extraction, and analytics. It runs locally over stdio or as a networked Streamable HTTP service with OAuth2 sign-in and provider auto-detection.21 npmISC
- AlicenseNot gradedqualityBmaintenanceEnables AI agents to search and read email, inspect attachments, and optionally send messages and organize folders through existing IMAP and SMTP mailboxes. Supports Docker, stdio, and HTTPS OAuth deployment with read-only or full-access modes.MIT