Zoho Mail Read-Only Container Client
Read-only integration with Zoho's hosted Mail MCP for a pinned primary mailbox: inventories and calls only approved read tools such as getMailAccounts, getAccountDetails, listEmails, SearchEmails, getMessageContent, getMessageAttachmentInfo, getAllFolders and getFolder, returning sanitized status and bounded metadata rather than raw mailbox data. A separate REST adapter retrieves bounded aggregate DMARC report attachments from a pinned folder/domain using user-authorized ZohoMail.messages.READ and ZohoMail.accounts.READ scopes. No sending, deleting, marking read or third-party report upload is supported.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@Zoho Mail Read-Only Container Clientlist my last 10 emails in Zoho Mail"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
Zoho Mail read-only container client
An outbound-only, one-shot local client for Zoho's official hosted Mail MCP, with a separate REST adapter for bounded aggregate DMARC attachments. No listener, public endpoint, port forwarding, mailbox database, sending, deleting, marking read or third-party report upload. The machine must be online for local tasks. This is not an automatic ChatGPT connection or task-delivery service.
Status and authorization
The private owner launcher supplies a trusted mcpHost pin alongside mcpUrl in the stdin configuration. Dedicated custom servers can use a server-specific zohomcp.com hostname, as documented in Zoho's custom-server overview. The hostname is separately reviewed and approved, never copied from a tool argument or inferred from a URL. The client requires that exact host and exact endpoint for MCP requests; it rejects other tenants, lookalikes, extra subdomains, nonstandard ports, userinfo and redirects. The fallback console-host policy supports existing configurations only. No wildcard request matching is permitted. Owner identifiers and policy files must stay outside this repository/image.
inspect-auth.mjs reads the same private configuration on stdin and performs MCP initialize with HTTP POST and Accept for JSON/SSE. It recognizes successful bounded JSON or SSE initialization without requiring an OAuth challenge. It prints numeric HTTP status, allowlisted content type and fixed MCP/error categories, never URLs, server names, session IDs, messages, tokens or raw bodies. Only an actual 401/403 advertised challenge triggers bounded metadata GETs. Protected-resource metadata must stay on the exact owner origin; separately allowlisted authorization metadata receives no Authorization header. Discovery does not register a client, exchange tokens, initiate consent or call mailbox tools. Initialization success is not proof of Mail tool authorization; existing provider-managed connections may already supply that authorization. Do not change authorization type or create extra OAuth grants simply because no challenge appears.
Offline tests exercise the official MCP SDK with synthetic protocol responses, account/tool policy and bounded XML/gzip/ZIP parsing. Live authorization and provider schemas are unverified. OAuth grants and persistent credential setup are deliberately absent: endpoint possession alone does not establish successful OAuth authorization. A challenge stops with mcp_authorization_required; the client does not create accounts/clients/grants, launch browser OAuth or persist/refresh MCP OAuth tokens. An already authorized access token can be supplied transiently. Unattended MCP OAuth lifecycle support must be completed and verified before production scheduling.
Zoho documents custom HTTP/Streamable clients and separate OAuth authorization. Configure a dedicated Mail server by selecting individual read tools, not broad category checkboxes: getMailAccounts, getAccountDetails, listEmails, SearchEmails, getMessageContent, getMessageAttachmentInfo, getAllFolders, getFolder, choosing only those needed. Client discovery rejects any configured write or unknown tool. Provider prefixes, argument schemas and account response shape must be verified after authorization; the client refuses mismatches rather than guessing mappings. It verifies the pinned primary mailbox via getMailAccounts before scoped reads. Alias routing is handled by that mailbox, not a second account.
Zoho Mail MCP does not download attachments. The REST helper requires separately user-authorized ZohoMail.messages.READ,ZohoMail.accounts.READ, with the .com region fixed. It independently checks the primary mailbox. Do not extract existing credentials from another deployment or service. No additional folder grant is needed when the report folder ID is confirmed by the owner.
Official sources: Mail tools, server authorization, custom clients, attachment limitation, official MCP SDK.
Related MCP server: rubit-mcp-mail
Development
The owner-run owner-read-check.mjs reuses the private stdin configuration and official SDK. It inventories tools, refuses writes/unknown tools, reports sanitized names and schema compatibility, and calls only runtime-discovered exact getMailAccounts/listEmails names when their schemas support the approved arguments. It matches the trusted owner primary address, keeps the discovered account ID only in memory, and requests at most one message. It returns only fixed status, owner-match boolean and zero/one count; no account IDs, addresses, subjects or message bodies. Unsupported names/schemas stop for review rather than guessed mappings. This is not a scheduler or a dot delivery service.
Six exact runtime-observed aliases are reviewed: ZohoMail_getMailAccounts, ZohoMail_getAccountDetails, ZohoMail_listEmails, ZohoMail_SearchEmails, ZohoMail_getMessageContent, and ZohoMail_getMessageAttachmentInfo. These map to existing allowed read semantics and retain their original runtime name on the wire. There is no general namespace stripping. Other prefixed names require explicit review; canonical and alias duplicates are refused. Canonical flat arguments map to the observed nested path_variables and query_params wrappers for account details, listing, search and content retrieval. Listing selects only the schema-documented messageId field; nested listing/search default to limit one and reject limits above 50. Writes, unknown arguments, unsupported schemas and owner mismatches stop before message retrieval.
Transport failures include only safe HTTP status, fixed request stage and MIME category, bounded Retry-After seconds, and an allowlisted error code. Network errors and timeouts remain distinct from HTTP rejection and 401/403 authorization challenges. An endpoint-pattern category contains no URL or key. The client requests Streamable HTTP and does not guess alternate routes or fall back to another transport. POST 405 is reported as rejection; GET 405 remains the supported indication that an optional server stream is unavailable. Provider error bodies are bounded to 8 KB and two seconds for code recognition, then discarded; no raw header, body or URL is reported.
Use the committed devcontainer. On Windows Podman use devcontainer up --docker-path PATH_TO_PODMAN --workspace-folder PROJECT then devcontainer exec --docker-path PATH_TO_PODMAN --workspace-folder PROJECT npm test. Dependencies use a dedicated Linux volume because chmod on Windows bind mounts can fail. The :U option applies only to that dependency volume; it does not change host source ownership. No engine socket is mounted into the development container. Use npm ci --ignore-scripts, not floating dependency updates.
Node 22 base image and GitHub Actions are pinned by verified digest/commit. Runtime dependencies: official client-only @modelcontextprotocol/client 2.3.0, strict XML parser sax 1.6.1 and ZIP parser fflate 0.8.3; transitive versions/integrity are in package-lock.json. CI runs tests in a container, then builds and publishes to GHCR using job-scoped GITHUB_TOKEN with only contents:read/packages:write. It does not publish pull-request images, use credentials as build inputs, or need a new PAT. A dependency audit with zero advisories is point-in-time evidence, not a guarantee.
Private runtime configuration
All configuration is transient and separate from source/image. cli.mjs accepts exactly one private JSON envelope on stdin: config plus request. Never put the full MCP URL, token or REST credentials in shell arguments, logs, CI secrets, source or ordinary chat. Endpoint URLs can themselves contain API keys. No input-envelope file is necessary. MCP accepts only HTTPS mcp.zoho.com, exact endpoint path/query, verified certificates and no redirects; provider-host changes require review.
Policy fields are accountId, primaryEmail, dmarcFolderId, dmarcDomain; MCP credentials are mcpUrl and optional mcpBearerToken; REST fields live under rest as ZOHO_CLIENT_ID, ZOHO_CLIENT_SECRET, ZOHO_REFRESH_TOKEN. Folder/domain are pinned per invocation, cannot be overridden in tool arguments, and must be owner-confirmed. Keep all actual identifiers/configuration private.
Requests: { "action": "list_tools" }, { "action": "call", "name": "listEmails", "arguments": { "accountId": "123", "limit": 1 } }, or { "action": "dmarc", "messageId": "789" }. Use discovered schemas before real calls. Examples are synthetic.
After explicit setup authorization, a user can run the container's ephemeral hidden prompt: podman run --rm -it --read-only --cap-drop=ALL --security-opt=no-new-privileges --pids-limit=64 --memory=256m --entrypoint node IMAGE@sha256:DIGEST interactive.mjs. Secret input is not echoed or saved. Do not run this in a recorded terminal. This prompt does not obtain new OAuth authorization or solve unattended credential persistence. Persistent encrypted storage/runtime identity and any OAuth consent must be approved separately; do not assume Podman's default secret store is encrypted. A reviewed same-user Windows protected-store bridge could supply private stdin after approval, without exposing secrets to the agent or environment.
For automation, a trusted launcher sends the private envelope to podman run --rm -i --read-only --cap-drop=ALL --security-opt=no-new-privileges --pids-limit=64 --memory=256m IMAGE@sha256:DIGEST. Safe bounded output is stdout, not a mailbox log. This repository does not configure that launcher, Windows tasks or conversation delivery.
Limits and maintenance
DMARC: three attachments/message, 512,000 compressed bytes/attachment, 2,000,000 expanded bytes, one flat XML entry/ZIP (stored/deflate), 1,000 records, 30,000 nodes, depth 20, field text 1,024 characters. Strict UTF-8, DTD/custom entities/processing instructions rejected, no external entity fetching. Nested archives/traversal, malformed required fields and wrong domains fail. Full XSD and ZIP CRC validation are not provided. Results are untrusted descriptive report data, not proof of authenticity or attack. Identical report IDs deduplicate within one message; changed aggregate content with the same ID is flagged. No cross-message state or retention. Larger analytics/retention needs favor a maintained DMARC analyzer after separately approving data disclosure.
HTTP/MCP responses cap at 1.5 MB before parsing; projected MCP output caps at 100 KB, 50 items per array and 50 KB per string. REST token refresh and Mail calls time out; mailbox/account responses are not cached. Only access tokens are reused in process memory, with single-flight refresh, expiry margin and failure cooldown. A one-shot container loses that cache on exit, so frequent separate REST runs can hit Zoho's refresh throttle. Do not retry rapidly: after documented throttle wait ten minutes before one probe. Never create a persistent plaintext token cache as a workaround.
Preserve the last verified image digest; pull a proposed digest, run offline/small live checks, then update the approved launcher. Rollback selects the previous digest and does not restore old grants or secrets. Revoke/rotate MCP endpoint keys and Zoho grants in the user's official consoles; update the separately approved credential bridge securely. Confirm outstanding access-token validity windows with the provider. Review pinned dependencies, base image and CI actions periodically, test changes in the devcontainer, inspect publication contents/history and publish a new immutable image. No automatic updater is configured.
Keep existing deployments/schedules until live Mail, DMARC and the outbound runner/result-delivery channel pass. Cloud schedules cannot directly invoke a private offline host. No new public inbound endpoint should be introduced to bridge that gap.
This server cannot be deployed
Maintenance
Related MCP Connectors
Read-only IMAP email for your AI agent, scoped to the mailboxes you choose, with built-in progress.
Hosted agent inboxes: read mail, send to owner-approved recipients. Mail is server-readable.
Free, read-only email and DNS checks: SPF, DKIM, DMARC, MX, DNS records, blocklists, domain health.
- Lettio MCPOAutheu.lettio
Private, EU-hosted email for AI agents over JMAP: read, search, reply, organize, send.
Related MCP Servers
- AlicenseNot gradedqualityAmaintenanceEnables MCP-compatible AI assistants to securely search multiple mailboxes, reconstruct email threads, and inspect attachments through read-only tools without altering mailbox state.Apache 2.0
- AlicenseAqualityAmaintenanceEnables read-only, provider-agnostic email access over IMAP, allowing users to list folders, search and read messages, and download attachments without ever marking messages as read.6MIT
- AlicenseNot gradedqualityCmaintenanceEnables a locally-running agent to read and organize email from Gmail, Microsoft 365, and IMAP mailboxes with restricted, audited access.GPL 3.0
- FlicenseNot gradedqualityBmaintenanceEnables read-only access to multiple email accounts, including Gmail via App Password and generic IMAP providers, so an agent can list accounts and folders, search messages, and fetch messages or attachments. All operations are strictly read-only, with no sending, writing, deleting, or moving.1-