# The mailbox user path Connecting the chat client you already use, and what each tool returns and bounds. This is one reading path through the MailFathom user guide, every page of it in one file and in the order the guide walks them. `llms.txt` beside this file maps the whole of the documentation, and every link below is relative to the directory that map sits in. # Connecting the chat client you already use [Getting started § connect an MCP client](users/getting-started.md#7-connect-an-mcp-client) states the two facts every client needs — the Streamable HTTP endpoint at `/mcp`, and the key as a bearer credential — and that is the complete answer for a client you configure by writing a file. This page answers what comes next and what no reference page can answer, because the answer belongs to somebody else: **where the dialog is in the product I actually use, what it accepts as an address, and what it accepts as a credential.** The interesting part is not the URL. Three things differ per client, and each decides whether the connection is possible at all rather than how tidy it is: - **Where the client runs.** A client on your own machine reaches a loopback address; a client that runs in its vendor's cloud reaches only an address published on the public internet. - **Whether it can send a static `Authorization` header.** Two of the clients below cannot, which means a deployment authenticated by an API key cannot be connected from them at all — no address and no header spelling changes that. - **Which transport it speaks.** The endpoint serves Streamable HTTP and nothing else, so a client limited to the legacy HTTP+SSE transport is blocked here. A reader who does not know the second one reads a `401`, or a connector that lists no tool, as a MailFathom defect. ## Two claims this page does not make **Presence is a check at a point in time, not a supported-client list.** Every entry below is what that product's own current documentation said on the date the entry carries. None of these products is under this project's control, and any of them may change its dialog, its authentication choices, or its transports next week without anybody here touching anything. An entry that has stopped being true is a defect in this page rather than in the deployment that trusted it. **Absence is not a refusal.** A client missing from this page is not blocked, unsupported, or known to fail — it is unchecked. MailFathom serves any client that speaks the MCP Streamable HTTP transport, and [any other client that speaks Streamable HTTP](#any-other-client-that-speaks-streamable-http) is the section for one. ## What the evidence column means Each entry says which of two kinds of evidence it rests on, because they are not the same claim: - **Documented** — the product's own current documentation was read on the date the entry carries. It establishes what the vendor publishes about the dialog, the address, and the credential. It does not establish that a connection was made. - **Observed** — a MailFathom deployment was genuinely connected from that client and behaved as the entry says. **Every entry on this page today is `Documented`.** That is deliberate rather than a gap waiting to be filled: none of these clients is something this repository can run in a test, and an entry claiming otherwise would be asserting a verification that nothing here performs. ## What is the same for every client Four things hold whichever client is being configured, and each is stated in full on the page that owns it rather than repeated per client below: - **The address ends in `/mcp`**, which is a constant rather than a setting. What precedes it is where the deployment is served: `http://127.0.0.1:8080` under the Compose defaults, or the public HTTPS address your proxy or [MailFathom's own TLS](operations/mcp-endpoint.md#https-and-your-own-domain) serves. - **An API key travels as `Authorization: Bearer `**, and nothing else about it is client-specific. [The MCP endpoint § API keys](operations/mcp-endpoint.md#api-keys) records what a key is, how it rotates, and what every refusal looks like. - **A browser-based client is also subject to the origin policy.** A client that runs as a web page sends an `Origin` header, and a deployment that narrowed `McpEndpoint:Cors:AllowedOrigins` has to list that client's origin; [CORS and the `Origin` header](operations/mcp-endpoint.md#cors-and-the-origin-header) holds the rule. A client that is a desktop application or a command-line tool sends none and is unaffected. - **Enabling the endpoint at all, and choosing what protects it, is one decision made before any of this.** [Getting started § enable the MCP endpoint](users/getting-started.md#6-enable-the-mcp-endpoint) is the short form and [the MCP endpoint](operations/mcp-endpoint.md) is the whole contract. ## The clients, in one table | Client | Where it runs | The address it needs | Authentication it offers | Evidence | | --- | --- | --- | --- | --- | | [The ChatGPT web application](#the-chatgpt-web-application-in-developer-mode) | The vendor's cloud | Public HTTPS | OAuth, or none — **no static header** | Documented, 2026-08-12 | | [The Claude applications](#the-claude-applications-on-the-web-and-on-the-desktop) | The vendor's cloud | Public HTTPS | OAuth, or none — **no static header** | Documented, 2026-08-12 | | [The Claude Code command-line tool](#the-claude-code-command-line-tool) | Your machine | Loopback or public | Static header, or OAuth | Documented, 2026-08-12 | | [Visual Studio Code with GitHub Copilot](#visual-studio-code-with-the-github-copilot-chat-agent) | Your machine | Loopback or public | Static header, or OAuth | Documented, 2026-08-12 | | [The Cursor editor](#the-cursor-editor) | Your machine | Loopback or public | Static header, or OAuth | Documented, 2026-08-12 | Every client in the table speaks the MCP Streamable HTTP transport, which is the one MailFathom serves, so none of them is blocked on transport today. That column would only be worth a table of its own for a client that speaks the legacy HTTP+SSE transport alone; MailFathom serves no such endpoint, and a client limited to it cannot connect at all. **The two `no static header` rows are the finding this page exists for.** Both products connect a remote MCP server through a dialog that offers OAuth or nothing, so the deployment shape most readers start from — one API key, one client — has no way to present its credential there. Neither is a MailFathom limitation and neither has a workaround worth writing down: the endpoint's answer for those clients is [an OAuth entry](operations/mcp-endpoint.md#oauth) beside or instead of the key, and [MCP client OAuth](operations/mcp-client-oauth.md) is the sequence that gets one working — the identity provider's side, which is where nearly all of that work is. ## The ChatGPT web application, in developer mode OpenAI documents connecting a remote MCP server of your own as a *developer mode* app rather than through the ordinary connector list, and states the feature is available to Pro, Plus, Business, Enterprise, and Education accounts on the web. **Where the dialog is.** Turn the feature on under **Settings → Security and login → Developer mode**, then open **ChatGPT Plugins**, select the plus button, and create a developer-mode app pointing at your server URL. A created app appears under **Drafts**, where its tools can be toggled individually and refreshed to pull the current tool list from the server. **The address has to be public.** The app is connected by OpenAI's own infrastructure rather than by the browser in front of you, so a loopback address reaches nothing and a deployment on a private network is unreachable however well it works from your desk. Serve the endpoint over HTTPS on an address that resolves publicly before starting here. **An API key cannot be presented.** OpenAI documents three authentication modes for a developer-mode app — OAuth, no authentication, and a mixed mode combining the two — and no field for a static header or an API key. So a MailFathom deployment whose `McpEndpoint:Authentication` list holds only `ApiKey` entries cannot be connected from this client, and the two shapes that can are [an OAuth entry](operations/mcp-endpoint.md#oauth) and [an endpoint requiring no credential](operations/mcp-endpoint.md#requiring-no-credential) — the second of which is the wrong answer on a public address, and says so in a startup warning. **Transport.** OpenAI documents SSE and streaming HTTP as the supported MCP protocols, so the Streamable HTTP endpoint is served as-is. **This is the one client the endpoint documentation carries a worked certificate profile for.** OpenAI publishes a managed client certificate for its connector, and a deployment can require it in addition to whatever credential it asks for; [the ChatGPT connector profile](operations/mcp-endpoint.md#the-chatgpt-connector-profile) is the configuration and the reason it composes with OAuth rather than replacing it. Sources: [ChatGPT developer mode](https://developers.openai.com/api/docs/guides/developer-mode), [Building MCP servers for plugins and API integrations](https://developers.openai.com/api/docs/mcp). ## The Claude applications, on the web and on the desktop Anthropic calls a remote MCP server a *custom connector*, and documents the feature as available on Claude, Cowork, and Claude Desktop for Free, Pro, Max, Team, and Enterprise plans. A connector belongs to the account rather than to one application: Anthropic's own documentation for the command-line tool below records that connectors added on the web load there too, for whoever is signed in with that account. **Where the dialog is.** On an individual plan, **Customize → Connectors → + → Add custom connector**, then enter the remote MCP server URL. On a Team or Enterprise plan an owner adds it instead, under **Organization settings → Connectors**, and members reach it from their own **Customize → Connectors**. **The address has to be public.** Anthropic documents that the server must be reachable over the public internet from its own IP ranges, and that a server on a private corporate network, behind a VPN, or blocked by a firewall will not connect even when it is reachable from your own machine. A loopback address cannot be a custom connector. **An API key cannot be presented.** The dialog takes the URL, and an **Advanced settings** panel taking an OAuth client identifier and client secret. There is no field for a static header, so — exactly as with the client above — an API-key deployment cannot be connected from here, and [an OAuth entry](operations/mcp-endpoint.md#oauth) is what this client is served with. **The desktop application's configuration file is a different thing.** `claude_desktop_config.json` configures local stdio servers that the application starts as processes on your machine. MailFathom is a service reached over HTTP rather than a process to start, so it is a connector rather than an entry in that file. **Transport.** Anthropic documents both Streamable HTTP and SSE as valid transports for a custom connector, so the endpoint is served as-is. Sources: [Get started with custom connectors using remote MCP](https://support.claude.com/en/articles/11175166-get-started-with-custom-connectors-using-remote-mcp), [Build custom connectors via remote MCP servers](https://support.claude.com/en/articles/11503834-build-custom-connectors-via-remote-mcp-servers). ## The Claude Code command-line tool This client runs on your machine, so it reaches a loopback address, and it takes a static header on the command line — which makes it the shortest path from an API-key deployment to a working tool call. ```bash claude mcp add --transport http mailfathom http://127.0.0.1:8080/mcp \ --header "Authorization: Bearer $(cat mcp-workstation-key)" ``` **What each part is.** `--transport http` is the Streamable HTTP transport; `mailfathom` is a name you choose, and it is what labels the tools in a session; the URL is the endpoint. The server is registered for the current project by default — add `--scope user` to register it once for every project, or `--scope project` to write it into the repository's own `.mcp.json`. **Verify it before opening a session.** `claude mcp list` reports the server as `✔ Connected` once it has answered. Two other statuses are worth reading rather than retrying: `! Needs authentication` means the endpoint answered `401` or `403` and no credential was configured for it, and `✘ Failed to connect` covers a rejected header as well as an unreachable address — Anthropic documents that a server which rejects a configured `headers.Authorization` is a failed connection rather than falling back to an OAuth flow, so a mistyped key looks like a broken address until you read the detail on the status. **OAuth instead of a key.** Add the server without `--header`, then run `/mcp` inside a session and choose `Authenticate`, or run `claude mcp login mailfathom` from the shell. The endpoint's protected-resource metadata is what directs the client at your authorization server. Sources: [Connect to MCP servers](https://code.claude.com/docs/en/mcp-quickstart), [Connect Claude Code to tools via MCP](https://code.claude.com/docs/en/mcp). ## Visual Studio Code with the GitHub Copilot chat agent MCP servers are configured for the chat agent in an `mcp.json` file, reachable from the command palette as **MCP: Add Server**, which asks whether the entry belongs to the workspace or to your user profile. The editor runs on your machine, so a loopback address works. Write the key as an input rather than into the file, so the file itself carries no credential: ```json { "inputs": [ { "type": "promptString", "id": "mailfathom-key", "description": "MailFathom MCP API key", "password": true } ], "servers": { "mailfathom": { "type": "http", "url": "http://127.0.0.1:8080/mcp", "headers": { "Authorization": "Bearer ${input:mailfathom-key}" } } } } ``` `"type": "http"` is the Streamable HTTP transport. Microsoft's own guidance is the reason for the `inputs` block: hardcoding an API key in the file is what it tells you to avoid, and a workspace `mcp.json` is commonly committed. **OAuth instead of a key.** Replace the `headers` block with an `oauth` block naming the client identifier your authorization server issued; Microsoft documents that the editor then runs the flow itself. **One thing to check in a remote workspace.** A server defined in workspace settings runs where the workspace does, so a deployment on your laptop is not reached from a workspace opened on a remote machine. Define the entry in the settings of whichever side can actually reach the address. Sources: [Use MCP servers in VS Code](https://code.visualstudio.com/docs/copilot/customization/mcp-servers), [MCP configuration reference](https://code.visualstudio.com/docs/agents/reference/mcp-configuration). ## The Cursor editor Cursor reads `~/.cursor/mcp.json` for every project and `.cursor/mcp.json` for one, and its own documentation lists stdio, SSE, and Streamable HTTP as the transports it speaks. It runs on your machine, so a loopback address works. ```json { "mcpServers": { "mailfathom": { "url": "http://127.0.0.1:8080/mcp", "headers": { "Authorization": "Bearer ${env:MAILFATHOM_MCP_KEY}" } } } } ``` `${env:NAME}` is Cursor's own interpolation, resolved in `url` and `headers` among other fields, which is how the key stays out of a file that is commonly committed with the project. **OAuth instead of a key.** Cursor takes an `auth` block carrying a client identifier, a client secret, and the scopes to request, in place of the `headers` block. Sources: [Model Context Protocol (MCP)](https://cursor.com/docs/mcp). ## Any other client that speaks Streamable HTTP There is no entry to look up here, and that is the point: MailFathom serves the MCP Streamable HTTP transport at `/mcp` and asks for whatever its `Authentication` list configures, so any client implementing that transport connects with the two facts [getting started](users/getting-started.md#7-connect-an-mcp-client) already states. Three questions decide whether a given client is one of them, and each has an answer above rather than a setting to find: - **Can it send a header you choose?** If yes, an API key is the whole configuration. If no, the deployment needs an OAuth entry, whatever the client is called. - **Where does it run?** A client in somebody else's cloud needs an address published on the public internet, and serving a mailbox there is a decision about TLS and authentication rather than about the client. - **Does it speak Streamable HTTP?** A client limited to the legacy HTTP+SSE transport connects to nothing here. ## What a working connection looks like Whichever client was configured, a connected one lists at least four tools — `list_accounts`, `list_emails`, `get_email_content`, and `search_emails` — each advertising itself as read-only, non-destructive, and idempotent. Where that list is shown is the one client-specific part: `claude mcp list` or the `/mcp` panel for the command-line tool, the server's entry in the editor's MCP view for Visual Studio Code and for Cursor, the connector's own settings page for the two cloud clients. A fifth tool, `ask_mail`, appears only once a chat model and an embedding model are both configured and working. Its absence is the deployment saying it cannot answer questions yet rather than a connection fault, and no client setting changes it. Then ask the assistant to list recent mail, and read `folderFreshness` in the result before reading the emails: [getting started § make the first call](users/getting-started.md#8-make-the-first-call-and-read-it-correctly) is what that field means, and [using the tools](users/usage.md) is the day-to-day surface. ## When the connection is refused Every refusal below comes from the endpoint rather than from the client, so it means the same thing in all five products — what differs is only where each one shows it. [Verifying an enabled endpoint](operations/mcp-endpoint.md#verifying-an-enabled-endpoint) gives the same reading for a request made by hand. | What the client reports | What it is | Where to look | | --- | --- | --- | | `401`, or a prompt to authenticate | No credential, a credential that is not a bearer one, a key matching no entry, or a key whose `Lifetime` has passed | [API keys](operations/mcp-endpoint.md#api-keys) | | `403` from a browser-based client | The client's origin is outside `McpEndpoint:Cors:AllowedOrigins` | [CORS and the `Origin` header](operations/mcp-endpoint.md#cors-and-the-origin-header) | | `403` with no body, on an HTTPS deployment | A configured client-certificate profile refused the certificate, or none was presented to a `Required` profile | [Client certificates](operations/mcp-endpoint.md#client-certificates) | | `429` where `401` was expected | The shared anonymous rate-limit partition is exhausted, so the request never reached the point where a challenge is written | [Rate limiting](operations/mcp-endpoint.md#rate-limiting) | | `404`, or nothing at all | The address is wrong, the endpoint is not enabled, or a proxy in front of it does not route `/mcp` | [The endpoint is off by default](operations/mcp-endpoint.md#the-endpoint-is-off-by-default) | | A connection that never establishes, from a cloud client | The address is not reachable from the vendor's network — loopback, a private network, or a firewall | The two cloud sections above | A client refusing to accept the configuration at all — no field for a header, a transport it does not speak — is the other kind of failure, and it is the per-client section rather than this table that answers it. ## Related - [Getting started](users/getting-started.md) — the whole path from an installed instance to a first tool call - [The MCP endpoint](operations/mcp-endpoint.md) — every setting named here, with its rules: keys, OAuth, origins, TLS, client certificates, rate limits, and timeouts - [Using the tools](users/usage.md) — what each tool answers, what it bounds, and what its errors mean - [Configuring a mailbox at your provider](users/mailbox-providers.md) — the other half of a working deployment --- **Trademarks.** The product, service, and company names on this page are their owners' trademarks and are used solely to identify the client applications a MailFathom deployment can be connected from. Their use implies no affiliation with, sponsorship by, endorsement by, or certification from those owners, in either direction, and this page reproduces no third-party logo, icon, wordmark, or screenshot. OpenAI and ChatGPT are trademarks of OpenAI, Inc. Anthropic, Claude, and Claude Code are trademarks of Anthropic, PBC. Microsoft, Visual Studio Code, GitHub, and GitHub Copilot are trademarks of the Microsoft group of companies. Cursor is a trademark of Anysphere, Inc. [`THIRD_PARTY_LICENSES.md`](https://github.com/Krzysztof318/MailFathom/blob/main/THIRD_PARTY_LICENSES.md#trademark-and-brand-use) records the per-owner review this statement comes out of, and why it sits here rather than in `NOTICE`. # Using the tools MailFathom publishes five MCP tools, and together they are the whole surface: an agent can see which mailboxes exist, list mail, read one message, search, and ask a question — nothing else. This page is the user's view of that surface: what each tool answers, what every result carries, what the deliberate limits are, and how to read a failure. The full contracts — every argument, every field, every bound — live in [MCP tools](features/mcp-tools.md) and the feature pages it links, and this page does not restate them. Four of the five are always there. `ask_mail` needs a chat model and an embedding model configured and working, so a deployment that has neither does not offer it at all; its absence from a tool listing is that deployment saying it cannot answer questions rather than something being broken. ## The model behind every call A tool call reads the **local copy** that synchronization maintains. Nothing in a request reaches a mail server, so a call is fast, works while the server is unreachable, and cannot mark anything as read — locally or remotely. The price of that model is freshness, which is why every listing and every search carries `folderFreshness`: one entry per folder in scope, stating when synchronization last committed progress there, or that it never has. An agent that reads mail without reading that field will eventually present an empty folder as an empty mailbox. Results are bounded by design. A listing serves at most 100 summaries per page, a search at most 50 ranked matches, a body at most the configured character bound, a search extract at most a few dozen words. The bounds are the deployment's privacy control on how much mail one call can draw out, so they are refused rather than stretched when a caller asks for more, and none of them is a client argument to widen. ## `list_accounts` — which mailboxes exist Takes no arguments and returns the mailboxes this deployment serves. Call it first: every other tool narrows by account, and this is where the names to narrow with come from. Each entry carries two of them — the `accountId` an operator configured and the `displayName` they gave it — and **either one names the account** in a later call, the display name matched without regard to case. Quote the display name to a person; the identifier is what other results report and what stays stable if the readable name is changed. Each account also lists its folders with the same freshness statement a listing carries, and the result says whether synchronization is running at all. An account with no folders listed has never been synchronized, which is a different thing from a mailbox that holds nothing. Nothing about how MailFathom reaches a mailbox is returned: no server, no port, no user name, no credential. ## `list_emails` — the timeline Returns a page of summaries, newest received first by default, filtered by any combination of account, folder, sender, recipient, subject fragment, received range, seen state, and attachment presence. Every argument is optional; a bare call reads every folder every served account maps and lets tools read — configuration is what says which folders those are, and a folder it does not name is one this deployment does not have. A summary is enough to recognize a message — subject, sender, recipients, timestamps, size, attachment counts, remote flags — and carries `storedEmailId`, the identifier a content read uses. It names its account both ways, as `accountId` and `accountDisplayName`, so telling a person which mailbox a message came from needs no second call. Two fields prevent common misreadings: - `attachments` counts real attachments separately from inline images, so a message whose only payload is a logo in its signature does not read as one carrying a document. - `contentAvailability` says whether the raw content is stored locally, and why not when it is not, instead of failing a later read unexplained. A message larger than the configured size limit reports so here and will report the same on every later read; one that arrived while local storage stood at its ceiling reports that it is waiting for room, and a later synchronization run fetches it. Paging is a cursor: pass `nextCursor` back unchanged, with the same filters and direction. A cursor is bound to the filters that issued it, so changing a filter mid-walk is refused (`52002`) rather than answered with a page from a different question. **Junk is left out unless you ask for it.** Mail your provider or your own filter already set aside is not what a timeline is for, and an agent reading it cannot tell mail written to deceive it from correspondence. Set `includeJunkMail: true` when you are looking for a message a filter took; the result says which of the two listings you got, in `includedJunkMail`. The same argument and the same field are on `search_emails`. ## `search_emails` — ranked text search Takes `queryText` — the words to find — plus the same structured filters a listing has, and returns one window of matches ranked by relevance, each carrying the summary a listing would show and short extracts of the body around what matched, the matched runs wrapped in `**`. What it searches is the **lexical index**: subject, normalized participant addresses, and the extracted body text. Text inside attachment payloads is not indexed at all, and encrypted mail has no indexed body either. `subjectFragment` and `queryText` are different arguments doing different work: the fragment narrows which emails are eligible, the query text is what the eligible ones are ranked against. **Read `retrievalMode` on the result to know how the match was made.** `lexical` means by words rather than by meaning, so a query term that appears nowhere in a message will not find it. `hybrid` means the same ranking was combined with a search by embedding similarity, so mail whose meaning is close is found too — a search for a roof leak reaching the message that said water damage. Which one you get depends on whether the server has an embedding provider configured and reachable, and it can differ between two calls, which is why the field is on every response rather than something to look up once. **`semanticSearch` beside it says why a `lexical` answer was lexical.** `inactive` means the server does not embed mail at all, so lexical is what it is meant to do and nothing is wrong. `available` means it does and its provider is answering. `degraded` means it does but currently cannot reach the provider or its configuration is wrong, so these results are narrower than the server intends — retrying will not help, and it is the server's operator who has a credential or a declaration to fix. Recovery is automatic once they do. There is deliberately no search cursor. Relevance order moves as mail keeps arriving and indexing catches up, so a second page could silently skip or repeat matches; ask a narrower question instead of a longer window. ## `get_email_content` — up to ten messages Takes the `storedEmailId` values a listing or search returned — at most ten, each named once — and returns, for each one, normalized headers, the plain-text body, optionally a sanitized HTML body, and every attachment it carries by name, type, and size. Set `includeAttachmentDownloadLinks` to also receive, for each file, **a short-lived URL that fetches it** over ordinary HTTP. No response ever carries a file's bytes, so a message with a video in it costs the same as one with a note. Reading the top few results of a search is therefore one call rather than one per message. The entries come back in the order you named them, and each carries either `content` or a `failure` saying why there is none, so one message this deployment cannot serve does not discard the others. Seven parts of the result exist so that an agent does not misreport a message: - **Truncation is explicit, and says which limit cut.** Each body representation carries `truncatedBy` and the original character count, so a cut message is never summarized as a whole one. `bodyCharacterLimit` means the message is longer than any single call returns; `readCharacterBudget` means the messages named before it used up the call's shared budget, and naming fewer at once returns more of this one; `sensitiveContentScanCeiling` means the deployment scans mail for sensitive content and analyzed as much of this body as it may, so the rest is withheld from every call rather than served unscanned. - **An absent body has a reason.** `availability` distinguishes a message that displayed nothing from one MailFathom cannot decrypt, from one whose content the size limit deliberately kept out of storage, and from one that is simply waiting for storage room — which is the one case where asking again later returns the body. - **Attachments are described always, fetched through a link.** Every read tells you what each file is called, what it is, and how large it is, because that is what you decide against when choosing whether to fetch one; `attachments` is `[]` only when the message carries none. `attachmentCounts` says how many either way. - **A link is a secret with a deadline.** `downloadState` is `notRequested` when you did not set `includeAttachmentDownloadLinks`, `issued` when `downloadUrl` fetches the whole file until `downloadExpiresAt`, and `unavailable` when this server issues no attachment links at all — which only its operator can change. Anyone holding the URL can fetch that file without a credential, so fetch it once and do not log or store it; ten minutes is the usual window and half an hour the most any server may allow. When it expires, call `get_email_content` again for a new one. - **File names are sanitized.** An attachment name is untrusted text from the message; what is published is a bare, normalized name, with a flag saying whether it had to be rewritten. - **A too-long or repetitive list is refused, not trimmed.** More than ten identifiers, none at all, or the same one twice ends the call with a code rather than returning part of what you asked for. - **A `[redacted:…]` marker is not message text.** On a server whose operator switched sensitive-content scanning on, the body, the subject, and the display names come back with each detected credential or piece of personal data replaced by `[redacted:]`. Report it as material of that kind withheld rather than quoting it as words the sender wrote, and expect the same marker every time. Only the first 40 named participants of a message have their display name scanned; every one after that is published as an address with no name, so on such a server a missing `displayName` does not prove the sender wrote none. [Sensitive-content scanning](features/sensitive-content-scanning.md#reading-a-message-is-scanned-in-flight) records what is scanned. The HTML body, when requested, is aggressively sanitized — no scripts, no styles, no remote loads — and [email content](features/email-content.md) records exactly what survives. ## `ask_mail` — a question, answered with its sources Takes a question in ordinary words, optionally narrowed to accounts and folders, and returns prose plus the emails the answer was drawn from. A chat model conducts the run and looks up mail as it decides it needs context, so this is the one tool that costs a provider call and takes seconds rather than milliseconds. Ask it when the answer spans several messages; search when the messages themselves are what you want. The question is yours to write, not a query to construct: its words are not matched against the mail, and the lookups are the model's own. That is also why there is no sender or date filter here — one supplied would narrow every lookup without the model knowing why its searches came back empty. The model narrows its own lookups instead, by the same sender, recipient, subject, date, seen state, and attachment filters `search_emails` publishes, so a question about one person's mail or one week reaches that mail rather than competing for it in a ranking. What it can and cannot ask for is [Mail answering § What one lookup may ask for](features/mail-answering.md#what-one-lookup-may-ask-for). Junk is outside every lookup a run makes, and here there is **no** way to ask for it. The answer is written by a model from the mail it retrieved, so a message written to deceive a reader would arrive as ordinary correspondence with nothing left to notice it. Use `search_emails` with `includeJunkMail` when junk is what you are looking for. ### What leaves your instance when you ask Asking is the one thing MailFathom does that sends your mail somewhere else on demand, so what leaves is worth knowing before the tool is enabled at all. Exactly two things reach the chat endpoint the operator declared: - **your question**, as you wrote it, and - **the extracts the run retrieved** — for each message the model looked up: the extract itself, the stable identifier, the account and folder alias, the subject, and the received time. Beside them travels the run's own instruction, which is a constant of the build and says nothing about your mailbox. Nothing else goes: not the accounts and folders in scope as a list, not whole bodies, not raw MIME, not attachments, not the participants, the size, the flags, or the attachment summary a listing publishes. Those are dropped before the provider is reached rather than filtered out of a log afterwards. Nor is what does go left behind once the answer comes back. One of the two APIs a provider can be reached over would keep the call for a month and show it in that provider's console; MailFathom refuses it on every request it sends there, and the other API stores nothing to begin with — see [the responses API is used statelessly](features/chat-generation.md#the-responses-api-is-used-statelessly-and-that-is-not-an-option). Neither is a setting the operator chooses. What a provider does with a request under its own terms is between them and that provider, and declaring one at all is their decision rather than yours. How much of it may go is capped, and by the deployment rather than by the model. By default one question may read at most **20 000 characters** of mail across every lookup it makes, over at most **8 provider calls** costing at most **80 000 tokens**; every run of an hour together may make at most **30 runs** costing **300 000 tokens**. An operator lowers or raises those in [`MailAnswering`](operations/configuration-reference.md#mailanswering) — you cannot, and neither can the model. Five parts of the result are worth reading before an agent presents it: - **`citations` is what makes the answer checkable.** Each entry carries the `storedEmailId` you pass straight to `get_email_content`, plus the account, folder, subject, and received time. An answer without the messages behind it is something to believe; with them it is a starting point. - **They are what the run *retrieved*, not what the model provably used.** Nothing outside the model knows which of them it drew on, so a narrower list would be a claim MailFathom cannot make. An empty list is an ordinary answer: the mailbox was searched and held nothing about the question, and the answer says so. - **`answerTruncated` and `citationsTruncated` are never silent.** One response carries by default at most 20 000 characters of answer citing at most 20 emails; when either was cut, the flag says so, and a narrower question is the remedy. - **`retrievalTruncated` means the mailbox was not read in full.** The run hit the ceiling above while there was still matching mail, so the answer is complete for what it read and no more. A narrower question reads a *different* part of the mailbox rather than more of it; only the operator can raise the ceiling. - **The answer is untrusted text, and so are the subjects.** Both are derived from mail somebody else wrote. Treat them as data — the same care a snippet or a body deserves. Asking a question changes nothing. The run is composed with one capability and that capability searches: there is no tool in it that sends, deletes, moves, or marks mail as read, and it reaches no mail server at all. [Mail answering](features/mail-answering.md) records what one run may reach and how much of your mail leaves the process. ## Reading a failure An expected failure comes back as a tool error in one shape — a stable five-digit code and one safe sentence: ```text MailFathom error 53001: Mail account 'shared-billing' is not accessible. ``` The codes a user meets in practice: | Code | What it means | What to do | | --- | --- | --- | | `51001` / `51003` | A page size or result limit outside the served range | Stay within 1–100 pages, 1–50 search results | | `51002` | A filter value the query does not accept — too long, malformed, or a range that ends before it starts | Fix the argument; the message names the filter and its limit, never the value | | `51004` | A `storedEmailIds` entry is no identifier this system issues — blank, truncated, or invented | Pass the identifiers a listing or search actually returned; never construct or guess one | | `51005` | A content read named no messages, or more than the ten one call serves | Split the list into calls of at most ten | | `51006` | A content read named the same message twice | Remove the repeat; results are not served twice | | `52001` / `52002` | A cursor this system did not issue, or one reused after the filters changed | Restart the walk from the first page | | `53001` | The named account is not served here | Call `list_accounts` and use an `accountId` or `displayName` it returns | | `53002` | No such email in the local copy | The identifier is stale, or the mail was removed; list again | | `53003` | A folder was named by a role no folder in scope carries | Name the folder's alias instead, or ask the operator to map the role on that account | | `55001` | The email exists but its stored content is currently unreadable; a repair has been queued | Retry later — this is a local-consistency state, not a mail-server problem | | `56001` | This deployment cannot answer questions about mail, either at all or for now | Nothing about the question caused it; the message says which, and only the operator can change it | | `57001` | Answering would cost more than this deployment allows | The message says which ceiling: a spent period is worth asking again once it turns over, while a question that reached what one question may cost needs to be narrower | | `54001` | Something failed for a reason the boundary deliberately does not describe | The server log has the detail, correlated by the request's trace | `53002` and `55001` also reach you inside a *successful* `get_email_content` result, as the `failure` on the entry for the message they concern, because a content read answers for each message it was given rather than failing whole. Refusals are deliberately uninformative in one direction: an account that does not exist and an account that is not yours are the same answer, so the tool surface cannot be used to discover what a deployment serves. ## What the deployment sees, and what it does not Every call is logged with the tool name, the outcome, and the duration — never with a filter value, a query text, a subject, or any part of a result. What you search your own mail for stays out of the operator's log by contract, and [what the endpoint records](operations/mcp-endpoint.md#what-the-endpoint-records) is the precise statement. The flip side is worth stating plainly: snippets and bodies returned to an agent are mail content and travel to wherever that agent's model runs. Which model sees your mail is decided by the client you connect, not by MailFathom. `ask_mail` adds a second destination, and it is one the deployment chose rather than the client: extracts of the mail a run retrieves go to the chat endpoint its operator declared, bounded as [§ What leaves your instance when you ask](#what-leaves-your-instance-when-you-ask) states. Nothing of the question, the answer, the query the model wrote, or a retrieved passage reaches a log on the way — what the operator can read is how many runs and tokens a period spent, which are counts and nothing about what they were spent on. ## Expectations worth setting - **Freshness is the synchronization interval** — five minutes by default, per account, plus the length of the run itself. Mail sent a moment ago is not yet listable. - **The remote `\Seen` flag is an observation, not an effect.** Results report the flags the last synchronization run saw, with `wasObserved` saying whether any run has looked; reading through MailFathom never changes them. - **Removing an account from configuration makes its stored mail unreachable** through the tools, though the rows remain until removed. Disabling synchronization does not — the copy already stored stays readable. - **What happens to locally stored mail the server deleted is per account**: the default keeps a hidden tombstone, and a deployment can choose erasure instead. [IMAP synchronization](features/imap-synchronization.md) records both dispositions. --- Copyright © 2026 Krzysztof Kasprowicz Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. Project repository: https://github.com/Krzysztof318/MailFathom