MCP Server (Model Context Protocol)¶
Mail Archiver ships an optional MCP (Model Context Protocol) server that exposes the archived mail to AI agents over the Streamable HTTP transport. It mirrors the read-only REST API: same data, same per-user permission model, same access logging — just packaged as MCP tools instead of REST endpoints, so an LLM agent can discover and call them directly.
The point is to let an AI agent read archived mail without ever exposing mailbox credentials or talking to the mail provider. A scoped per-user API key is a capability to read the archive, not a credential to be the mailbox — it inherits only its owner's mailbox permissions, grants no write path, and can be revoked instantly without touching the underlying mail account.
⚠️ The MCP server is disabled by default. It must be explicitly enabled in configuration (see Enabling the MCP server). When disabled, the
/mcpendpoint returns404 Not Found.
The MCP server is read-only by design: it exposes tools to list accounts, list folders, search messages, read a message and download an attachment. It never creates, modifies or deletes data.
Table of contents¶
- Enabling the MCP server
- Authentication
- Transport and endpoint
- Tools
- list_accounts
- list_folders
- search_emails
- get_email
- get_attachment
- Rate limiting
- Access logging
- Connecting an agent
- Security guidance for internet exposure
Enabling the MCP server¶
The MCP server is configured under the Mcp section of appsettings.json (or
the equivalent environment variables, e.g. Mcp__Enabled=true):
"Mcp": {
"Enabled": false,
"AllowAttachmentDownloads": true,
"DefaultPageSize": 20,
"MaxResults": 100,
"MaxAttachmentBytes": 10000000,
"RateLimitPerMinute": 120
}
| Setting | Default | Description |
|---|---|---|
Enabled |
false |
Master switch. When false, the /mcp endpoint returns 404. |
AllowAttachmentDownloads |
true |
When false, get_attachment returns an error instead of attachment bytes. Metadata about attachments is still returned by get_email. |
DefaultPageSize |
20 |
Page size used by search_emails when the caller omits pageSize. |
MaxResults |
100 |
Upper bound for the search_emails page size; larger values are clamped. |
MaxAttachmentBytes |
10000000 |
Maximum attachment size (in bytes) get_attachment will return inline as base64; larger attachments are refused. |
RateLimitPerMinute |
120 |
Fixed-window request budget per API key per minute (see Rate limiting). |
Authentication¶
The MCP server authenticates with the same per-user API keys as the REST API, sent as a bearer token:
API keys are created and managed in the web UI under API Keys. A key inherits the exact permissions of the user who owns it (admin sees all accounts; a restricted user sees only their assigned accounts). Keys can be revoked or expired independently of the underlying mail accounts. See the REST API authentication section for the full key lifecycle and permission model — MCP keys are not a separate credential store.
When the MCP server is disabled, or no/invalid bearer token is presented, the
endpoint returns 404 / 401 respectively (it never redirects to the login
page).
Transport and endpoint¶
The MCP server uses the Streamable HTTP transport and is mounted at:
It runs in stateless mode (one JSON-RPC request per HTTP request), which is
the recommended mode for servers that do not need server-to-client requests
like sampling or elicitation. Tools are discovered via the standard MCP
tools/list request and called via tools/call.
Tools¶
list_accounts¶
Lists the mail accounts the current API key is allowed to access. Returns
id, name, emailAddress, provider, isEnabled and lastSync.
Parameters: none.
Result: array of account objects. Use the returned id with list_folders
and search_emails.
list_folders¶
Lists the folder tree of a single mail account, with per-folder email counts.
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
accountId |
integer | yes | The mail account id (from list_accounts). |
Result: array of folder objects, each with name, fullPath, totalCount,
level and recursive children. Returns an empty list if the account does not
exist or the key is not authorized for it.
search_emails¶
Searches archived emails by full-text query, date range, account, folder and direction.
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
q |
string | no | Full-text search query (subject, body, from/to). |
from |
string (ISO-8601) | no | Sent-on-or-after UTC date/time filter. |
to |
string (ISO-8601) | no | Sent-on-or-before UTC date/time filter. |
accountId |
integer | no | Restrict to a single mail account id. |
folder |
string | no | Restrict to a single folder full path (e.g. INBOX). |
direction |
string | no | incoming or outgoing. |
page |
integer | no | 1-based page number (default 1). |
pageSize |
integer | no | Page size; clamped to [1, MaxResults]. 0 uses DefaultPageSize. |
sortBy |
string | no | sentdate (default), receiveddate, subject, from, to. |
sortOrder |
string | no | asc or desc (default desc). |
Result: a paged object with items (array of email summaries), page,
pageSize, totalItems, totalPages. Each summary carries id, accountId,
subject, from, to, sentDate, isOutgoing, hasAttachments and
folderName. Pass a returned id to get_email for the full body and
attachment list.
get_email¶
Fetches the full content of a single archived email.
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
id |
integer | yes | The archived email id (from search_emails). |
Result: a single email object with all summary fields plus cc, bcc,
receivedDate, messageId, textBody, htmlBody and attachments (array of
{ id, fileName, contentType, size }). Returns null if the email does not
exist or the key is not authorized for its account.
get_attachment¶
Downloads a single email attachment as a base64 blob.
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
id |
integer | yes | The archived email id the attachment belongs to. |
attachmentId |
integer | yes | The attachment id (from get_email's attachments). |
Result: an object with id, emailId, fileName, contentType, size
and base64Data (the raw attachment bytes, base64-encoded). The tool returns an
error when Mcp:AllowAttachmentDownloads=false, when the attachment exceeds
Mcp:MaxAttachmentBytes, or when the key is not authorized for the email's
account.
Rate limiting¶
The MCP server has its own fixed-window rate-limit policy ("Mcp"), partitioned
by the non-secret API-key prefix (first 11 characters of the ma_... token),
falling back to the client IP when no bearer token is present. The budget is
configured via Mcp:RateLimitPerMinute (default 120 per minute). When
exceeded, the endpoint returns 429 Too Many Requests with a Retry-After
header. This is independent of the REST API's rate-limit policy.
Access logging¶
Every tool call is logged through the same IAccessLogService as the REST API,
with the owning username and the matching access type (Search, Open,
Download). MCP activity is therefore visible on the Logs page alongside
REST API and web-UI activity, for a single audit trail.
Connecting an agent¶
The MCP server speaks the standard MCP Streamable HTTP transport, so any MCP
client that supports HTTP can connect. The URL is the application base URL plus
/mcp, e.g. http://localhost:5179/mcp, and the API key is supplied via the
Authorization: Bearer ma_... header.
For agents configured via the standard MCP client JSON, an entry looks like:
{
"mcpServers": {
"mail-archiver": {
"url": "https://archive.example.com/mcp",
"headers": {
"Authorization": "Bearer ma_<your-api-key>"
}
}
}
}
Security guidance for internet exposure¶
The MCP server is designed so that, with the defaults and the hardening below
in place, archived mail cannot leave the system without a valid per-user API
key that has been explicitly issued to an account. The points below are
mandatory reading before exposing /mcp to the internet.
Mandatory (block exploitation / data exfiltration)¶
- HTTPS only. Terminate TLS at the edge (reverse proxy). A bearer API key over plain HTTP can be sniffed and reused.
- Keep
Mcp:Enabled=falseunless you actually use it. When disabled,/mcpreturns404and no code path is reachable; this is the safe default and the safe state for deployments that do not need AI-agent access. - Rate limiting is enforced on the endpoint. The
"Mcp"policy (Mcp:RateLimitPerMinute, default120) is applied on the mapped/mcpendpoint. It is partitioned by the non-secret API-key prefix (first 11 chars of thema_...token) or the client IP when no bearer token is present. TuneRateLimitPerMinutedown for internet exposure (e.g.30–60) — MCP tool calls are heavier than REST calls and the endpoint should not be left at a high budget when untrusted clients can reach it. - Reverse-proxy
X-Forwarded-Formust be trusted carefully. The rate limiter falls back toRemoteIpAddress, which under a proxy is the proxy IP unlessUseForwardedHeadersis fed the real client IP. Ensure your proxy setsX-Forwarded-Forcorrectly and that only your proxy can set it; otherwise an attacker can spoof the rate-limit partition. - Do not place
/mcpbehind a CDN cache. MCP responses are per-request JSON-RPC and must never be cached — caching would both break the protocol and leak one user's result to another. Mark/mcpas uncacheable at the edge (Cache-Control: no-store).
API-key hygiene (block unauthorized access)¶
- Issue dedicated, short-lived keys for MCP agents. A single
ma_...key is valid for both/apiand/mcp; there is no per-protocol scope today. Therefore: - Create a separate key per agent (do not reuse a reporting-script key for an external LLM agent).
- Always set a short
ExpiresAtand rotate it; revoke immediately when the agent is decommissioned. - Prefer keys owned by a restricted (non-admin) user whose only assigned accounts are the ones the agent needs. An admin key grants visibility into all mail accounts; a restricted key limits blast radius.
- Treat the API key like a password. It is a bearer credential. Never embed it in client-side code, never log it, never commit it to a repo. Put it in the MCP client's secret store and rotate on suspected compromise.
- Audit on the Logs page. Every tool call is recorded in
AccessLogs(Search,Open,Download) under the owning user. Periodically review MCP activity for unexpected patterns (volume spikes, off-hours reads, mass downloads).
Trust model for returned data (block downstream harm)¶
- Email bodies and attachments are untrusted content, exactly as they were when they arrived in the mailbox. They may contain malware, phishing links, or active HTML/JavaScript.
get_emailreturnshtmlBodyraw and unsanitized. Do not render it to a human without sanitizing (DOMPurify /HtmlSanitizer/ a sandbox). Rendering raw archived HTML is a stored-XSS vector.get_attachmentreturns the raw bytes with the originalcontentType. Do not auto-open or auto-execute attachments; treat thecontentTypefield as untrusted metadata, not as a safe instruction.- Do not let the agent take write actions based on mail content without confirmation. A malicious email can contain prompt-injection text aimed at the consuming LLM. Enforce permissions and confirmation in the agent's own logic — the system prompt is not a security boundary.
Defense-in-depth recommendations¶
- Restrict
/mcpat the network layer when feasible (e.g. allow only the agent host's IP / VPN range at the reverse proxy), in addition to API-key auth. This is the single most effective control for internet exposure. - Lower
Mcp:AllowAttachmentDownloadstofalseif your agents do not need attachment bytes — attachment exfiltration is the highest-volume data path. Metadata (file name, content type, size) is still returned viaget_email. - Lower
Mcp:MaxAttachmentBytesto the smallest value your agents actually need (default10000000≈ 10 MB). This bounds the largest single response an API key can pull per request. - Lower
Mcp:MaxResultsandMcp:DefaultPageSizeto bound the largest search result an API key can pull per request (default100/20). - Monitor
AccessLogsand rate-limit rejections (429) operationally. A sudden spike from a single key prefix is a signal to revoke that key.