Skip to main content
Glama
tjacobs624

myMail-mcp

by tjacobs624

Email MCP Server for Cloudflare Workers

Fork notice. This repository is a fork of Xzese/email-mcp-for-cloudflare-workers (MIT), tracking upstream commit 0180c5c. Local changes: a Yahoo Mail provider preset, Claude Cowork / Claude Code setup instructions, a Yahoo and iCloud app-password guide, and an Outlook secret that is only required when Outlook is actually configured. Everything runs on Cloudflare's free tier — Workers, SQLite-backed Durable Objects, Workers KV, and Cloudflare Access (free up to 50 users).

A container-free remote MCP server for Gmail, Outlook, iCloud, Yahoo, and custom IMAP/SMTP accounts.

The Worker connects directly to IMAP and SMTP using Cloudflare outbound TCP sockets. Mailbox credentials are AES-256-GCM encrypted before being stored in Workers KV.

Table of contents

Related MCP server: simple-email-mcp

Public source, private deployments

This repository publishes the server source under the MIT License. It does not provide a shared, publicly accessible email service. Every operator deploys their own Worker, KV namespace, Cloudflare Access application, Microsoft Entra application, and secrets.

All configured mailboxes share one encrypted account store within a deployment. The server is therefore intended for one owner or a small group of mutually trusted users. Do not grant Cloudflare Access to unrelated tenants or users who should not share mailbox access.

The checked-in wrangler.toml is a sanitized example. For a local or manually initiated deployment, copy it to the ignored production configuration and replace every placeholder with identifiers from your own accounts:

cp wrangler.toml wrangler.production.toml

Never commit wrangler.production.toml, .dev.vars, private keys, certificates, Worker secrets, mailbox credentials, or OAuth client secrets.

For repository-connected Cloudflare builds, do not commit or upload wrangler.production.toml. The build generates an ignored wrangler.generated.json containing the real KV binding while preserving the runtime variables and secrets already configured in the Cloudflare dashboard. See Cloudflare repository builds.

Tools

  • email_add_account, email_list_accounts, email_remove_account

  • email_test_connection

  • email_list_folders, email_get_mailbox_status, email_get_all_account_mailbox_statuses

  • email_search_messages, email_search_all_accounts, email_list_all_inbox_messages, email_get_message, email_get_message_thread, email_get_message_attachment

  • email_update_message_flags, email_move_messages, email_archive_messages, email_move_messages_to_trash, email_delete_messages_permanently

  • email_create_message_draft, email_create_forward_draft, email_update_message_draft, email_send_draft

Every tool publishes an MCP output schema and returns validated structuredContent. A JSON text copy is also returned for compatibility with clients that do not yet consume structured output.

Local setup

Create .dev.vars:

CREDENTIAL_ENCRYPTION_KEY=base64-encoded-32-byte-key
ACCESS_LOCAL_DEV=true

Generate the encryption key with:

openssl rand -base64 32

Then run:

npm install
npm run dev

Connect an MCP client to http://localhost:8787/myMail/mcp.

Because wrangler.toml declares a route, wrangler dev simulates the route hostname rather than localhost, and the ACCESS_LOCAL_DEV bypass (which requires a loopback hostname) will not engage. Run wrangler dev --host localhost to develop against the bypass.

ACCESS_LOCAL_DEV bypasses Access verification only when the request hostname is localhost, 127.0.0.1, or ::1. Never configure it as a production Worker variable or secret.

MCP tool calls emit structured events to Cloudflare Workers Logs with the tool name, status, duration, and a safe error category. Tool arguments, upstream error text, credentials, and email content are not logged. View production events under Workers & Pages → email-mcp-server → Observability or stream local/deployed events with npx wrangler tail.

In VS Code, open Run and Debug, select Email MCP: Local server, and click Run (or press F5). Wrangler loads the same .dev.vars file automatically in the integrated terminal.

Deploy from the dashboard (no terminal)

The whole deployment works from the Cloudflare dashboard using Workers Builds, with no local Wrangler install and no build variables. Everything Wrangler needs is committed in wrangler.toml; the only thing set in the dashboard is the one real secret.

1. Create the Worker from this repository

Workers & Pages → Create → Workers → Import a repository, then select this repository and its default branch. Set the deploy command to:

npm run deploy

Use wrangler deploy, not wrangler versions upload: only deploy can apply the Durable Object migration this Worker needs on its first release.

Name the Workers Builds project to match the name field in wrangler.toml, or update that field to match the project, so Wrangler does not override it at build time.

2. Add the encryption key

Under Settings → Build → Variables and secrets, add:

Name

Type

Value

CREDENTIAL_ENCRYPTION_KEY

Secret

A base64-encoded 32-byte AES key

A Worker whose configuration is managed by wrangler.toml does not expose a separate runtime Variables and Secrets card in the dashboard, only the build section. Build secrets reach the deploy command's environment but are never bound to the running Worker, so the deploy script (scripts/deploy.mjs) promotes this one to a runtime secret with wrangler deploy --secrets-file, writing it to a private temporary file that is deleted when the deploy finishes. Secrets absent from that file are preserved from the previous version.

Everything else — the KV namespace ID, the Access team domain and audience — lives in wrangler.toml, because wrangler deploy treats that file as the source of truth for [vars].

3. Add the DNS record for the hub hostname

This Worker is attached with a route (mcp.timothyjacobs.net/myMail*) rather than a custom domain, so that other MCP servers can take their own paths on the same hostname later. Routes do not create DNS records, and without one the hostname never resolves and requests never reach the Worker.

In DNS → Records on timothyjacobs.net, add:

Type

Name

Content

Proxy status

AAAA

mcp

100::

Proxied

100:: is Cloudflare's reserved placeholder for originless setups. Because the record is proxied, requests never reach that address — the route intercepts them first.

4. First deploy

Trigger a build. The MCP endpoint is then https://mcp.timothyjacobs.net/myMail/mcp and the admin UI is at https://mcp.timothyjacobs.net/myMail.

Until Access is configured the Worker fails closed — the placeholder issuer and audience reject every request. That is expected at this stage.

4. Configure Access, then commit the real values

Follow Cloudflare Access to create the self-hosted application, enable Managed OAuth, and obtain the AUD tag and team domain. Edit wrangler.toml — directly on github.com is fine — replacing both placeholders:

[vars]
TEAM_DOMAIN = "https://<your-team>.cloudflareaccess.com"
POLICY_AUD = "<your-access-application-aud-tag>"

Committing triggers a redeploy. These are identifiers rather than credentials, so committing them to a public repository does not grant anyone access; if you would rather not publish them at all, make the repository private first.

5. Add mailboxes

Open the Worker's root URL, sign in through Access, and add each account with its provider button. See Yahoo and iCloud app passwords.

Cloudflare Access

Production authentication is handled by a Cloudflare Access self-hosted application with Managed OAuth. The Worker also verifies every Cf-Access-Jwt-Assertion signature, issuer, and audience before routing a request.

  1. Deploy the Worker and add the hub DNS record first. Until Access is configured, the placeholder issuer and audience make the Worker fail closed.

  2. In Zero Trust → Access controls → Applications, create a Self-hosted and private application scoped to the path this Worker occupies, not the whole hostname:

    Subdomain

    Domain

    Path

    mcp

    timothyjacobs.net

    myMail

    Scoping to the path keeps the rest of mcp.timothyjacobs.net free for other MCP servers, each with its own Access application and policy.

  3. Add an Allow policy restricted to your email or identity group. This server uses one shared encrypted account store, so do not authorize unrelated users.

  4. Configure one-time PIN or an identity provider. Enable MFA at the identity provider or in the Access policy where appropriate.

  5. Under the application's advanced settings, enable Managed OAuth. Configure only the redirect URIs required by your MCP clients; enable localhost or loopback redirects only when needed for local clients such as MCP Inspector.

  6. Copy the application AUD tag and your Zero Trust team domain into the ignored wrangler.production.toml:

[vars]
TEAM_DOMAIN = "https://your-team.cloudflareaccess.com"
POLICY_AUD = "your-access-application-aud-tag"
OUTLOOK_CLIENT_ID = "your-microsoft-entra-application-client-id"
OUTLOOK_TENANT = "consumers"

See Cloudflare's Secure MCP servers and Managed OAuth guides for the current dashboard and redirect-URI configuration.

Microsoft Entra app registration for Outlook

Outlook.com, Hotmail, Live, and Microsoft 365 accounts use interactive Microsoft OAuth. The Microsoft account that owns the app registration does not need to be the mailbox account. Each mailbox owner signs in and grants the app access when Outlook is selected in the account form.

  1. In the Microsoft Entra admin center, open Entra ID → App registrations → New registration.

  2. Enter any name and choose the supported account type:

    • For Outlook.com, Hotmail, and Live accounts only, choose Personal Microsoft accounts only.

    • To support both personal and work/school accounts, choose Accounts in any organizational directory and personal Microsoft accounts.

  3. After registering, copy Application (client) ID from Overview. Do not use the object ID, client-secret ID, or client-secret value as OUTLOOK_CLIENT_ID.

  4. Open Authentication → Add a platform → Web and add every callback URL from which the flow will be started. The path and port must match exactly:

    https://mcp.timothyjacobs.net/myMail/oauth/outlook/callback
    http://localhost:8787/oauth/outlook/callback

    The localhost entry is only needed for local development. Add another localhost URL if Wrangler runs on a different port. Leave both implicit grant checkboxes clear and leave Allow public client flows disabled; this server uses the authorization-code flow with PKCE.

  5. Open API permissions → Add a permission → APIs my organization uses → Office 365 Exchange Online → Delegated permissions, then add:

    • IMAP.AccessAsUser.All

    • SMTP.Send

    The Worker also requests openid, profile, email, and offline_access during sign-in. Organizational policies may require an administrator to grant consent; personal accounts can normally grant consent interactively.

  6. Open Certificates & secrets → Client secrets → New client secret. Copy the secret's Value immediately. Microsoft only displays it once. Do not copy the Secret ID.

Set the application client ID and appropriate Microsoft sign-in tenant in wrangler.production.toml:

[vars]
OUTLOOK_CLIENT_ID = "your-application-client-id"
OUTLOOK_TENANT = "consumers"

Use "consumers" for personal Microsoft accounts, "organizations" for work/school accounts, "common" for both, or a specific tenant ID for one organization. The app registration's supported account type must allow the value selected here.

For local development, put the client-secret Value in .dev.vars:

OUTLOOK_CLIENT_SECRET="your-client-secret-value"

For production, install it as a Worker secret instead of placing it in the TOML configuration:

printf '%s' 'your-client-secret-value' | npx wrangler secret put OUTLOOK_CLIENT_SECRET --config wrangler.production.toml

Common configuration errors:

  • unauthorized_client or “not enabled for consumers” means the app's supported account type does not allow personal accounts, or OUTLOOK_CLIENT_ID is not the Application (client) ID.

  • AADSTS50011 means the callback URL does not exactly match a configured Web redirect URI.

  • invalid_client during the callback usually means the secret is expired or the Secret ID was supplied instead of the secret Value.

See Microsoft's documentation for registering an application, redirect URIs, client credentials, and OAuth for IMAP and SMTP.

Production secret and deployment

Create and fill wrangler.production.toml as described above. The public example intentionally cannot be deployed until its placeholder KV namespace and Access values are replaced.

For an existing deployment, first confirm the required secrets are present:

npx wrangler secret list --config wrangler.production.toml

Do not replace an existing CREDENTIAL_ENCRYPTION_KEY; existing encrypted account records depend on it. For a brand-new deployment with no stored accounts, generate and install the key without writing it to disk:

CREDENTIAL_ENCRYPTION_KEY="$(openssl rand -base64 32)"
printf '%s' "$CREDENTIAL_ENCRYPTION_KEY" | npx wrangler secret put CREDENTIAL_ENCRYPTION_KEY --config wrangler.production.toml
unset CREDENTIAL_ENCRYPTION_KEY

Deploy after updating the Access variables:

npm run deploy

The production MCP endpoint is https://mcp.timothyjacobs.net/myMail/mcp.

Open https://mcp.timothyjacobs.net/myMail to manage email accounts through the Access-protected web interface. Credentials submitted there go directly from the browser to the Worker and do not pass through an MCP client or language model.

Connect ChatGPT

After deploying the Worker and configuring Cloudflare Access, add its MCP endpoint to ChatGPT:

MCP server URL: https://mcp.timothyjacobs.net/myMail/mcp
  1. Open Settings → Security and login and turn on Developer mode.

  2. Open Settings → Plugins and select Add custom plugin.

  3. Enter Email MCP Server as the name and https://mcp.timothyjacobs.net/myMail/mcp as the server URL.

  4. Select OAuth if prompted, then select Scan Tools and complete the Cloudflare Access sign-in.

  5. Select Create, then choose the plugin in a new chat.

Connect Claude

The Worker sits behind Cloudflare Access, which advertises OAuth to MCP clients. Both Claude surfaces complete the Access sign-in in a browser and store the resulting token themselves — no API key or bearer header to paste, and no credential ever reaches the model.

Endpoint: https://mcp.timothyjacobs.net/myMail/mcp

Claude Cowork / claude.ai

  1. Open Settings → Connectors and select Add custom connector.

  2. Name it Email and enter https://mcp.timothyjacobs.net/myMail/mcp as the URL.

  3. Select Add, then Connect on the new connector.

  4. Complete the Cloudflare Access sign-in in the popup. The connector turns green when the tool list loads.

Claude Code

claude mcp add --transport http email https://mcp.timothyjacobs.net/myMail/mcp

Then run /mcp inside Claude Code and choose Authenticate for the email server. A browser opens for the Access sign-in and the token is saved locally. Add --scope user to make the server available across all your projects instead of just the current one.

Verify with /mcp — the server should report as connected with its tools listed.

Yahoo and iCloud app passwords

Neither provider accepts your normal account password over IMAP, and neither offers third-party OAuth for mail. Both require a provider-issued app password, which you paste into the Worker's own Access-protected web UI at https://mcp.timothyjacobs.net/myMail — not into a Claude client, and not into this repository.

Yahoo — sign in, then Account Info → Account Security → Generate app password. Name it something like Email MCP and copy the 16-character value. If the option is missing, turn on two-step verification first.

iCloud — go to account.apple.com, then Sign-In and Security → App-Specific Passwords → Generate an app-specific password. This requires two-factor authentication on the Apple Account.

Add each account in the web UI with the matching provider button — Yahoo and iCloud prefill the correct hosts and ports:

Provider

IMAP

SMTP

Yahoo

imap.mail.yahoo.com:993 TLS

smtp.mail.yahoo.com:465 TLS

iCloud

imap.mail.me.com:993 TLS

smtp.mail.me.com:587 STARTTLS

The app password is encrypted with AES-256-GCM under CREDENTIAL_ENCRYPTION_KEY before it is written to KV, and is never rendered back into the page after saving.

Cloudflare repository builds

Cloudflare only receives files committed to the connected Git repository. Because wrangler.toml carries the KV namespace binding and the Access variables, a Git-connected build needs no build variables at all — set the deploy command to npx wrangler deploy and add CREDENTIAL_ENCRYPTION_KEY under Settings → Variables and Secrets.

The deploy command must be npx wrangler deploy. wrangler versions upload cannot apply Durable Object migrations, and this Worker declares one to create the MyMCP class, so an upload-only first deploy fails with "This Worker has a pending Durable Object migration". Once that migration has been applied by a real deploy, npx wrangler versions upload is usable for staging later changes without promoting them.

The Worker name in wrangler.toml must match the Workers Builds project name, otherwise Wrangler overrides it during the build and opens a pull request proposing the rename.

npm run cloudflare:deploy and npm run cloudflare:upload remain as aliases for those two commands, so a deploy command configured before the generator was removed keeps working.

Dependency updates

Dependabot checks npm packages and GitHub Actions weekly. Minor and patch updates are grouped into one pull request per ecosystem (npm-routine and github-actions-routine). Major updates remain individual pull requests and require manual review; the auto-merge workflow never selects them.

For same-repository Dependabot pull requests targeting main, the workflow reads Dependabot's metadata without checking out or executing pull request code. It queues squash auto-merge only for minor and patch updates. GitHub merges the pull request after every required check and ruleset condition succeeds, using the current pull request head commit.

Enable the following repository settings for this workflow:

  1. Under Settings → General → Pull Requests, enable Allow auto-merge.

  2. Configure the main ruleset to require branches to be up to date and these status checks: checks (the CI workflow, GitHub Actions app ID 15368) and Workers Builds: email-mcp-server (Cloudflare app ID 85455). Keep Cloudflare builds enabled for Dependabot branches; a missing required check blocks merging.

  3. If the ruleset restricts who can update main, it must allow the GitHub Actions identity used by the workflow to complete an approved auto-merge. Keep required checks enabled and do not use a bypass actor for the Dependabot workflow. An admins-only update restriction can leave auto-merge queued indefinitely.

Account settings

Prefer the web management interface at the Worker root to add, edit, or remove accounts. Editing can change the display name, email address, IMAP/SMTP hosts, ports, and TLS modes without replacing the stored password or OAuth tokens. The email_add_account tool remains available for clients that explicitly need programmatic setup. Each configured account also has a Test connection action that authenticates with its stored credentials and reports the connection result without exposing those credentials to the browser.

Use the exact folder path returned by email_list_folders. Search results default to newest first; set sortOrder to oldest for chronological order. Results include their folder path; pass that folder and IMAP UID to email_get_message or mutation tools.

email_search_messages returns a structured result with status, outcome, count, total, empty, messages, and an opaque nextCursor when another page is available. Pass nextCursor back as cursor with otherwise identical search criteria. An outcome of no_matches explicitly means the IMAP search completed successfully with zero matching messages; it does not indicate a connection or folder error.

email_list_all_inbox_messages lists message summaries from INBOX across all configured accounts, or the supplied accountIds, without requiring search filters. email_search_all_accounts applies the same summary shape with optional filters across INBOX by default or another supplied folder. Both return a flat message list with account identity on every message plus per-account totals, errors, and account-specific nextCursor values for follow-up single-account searches. email_get_all_account_mailbox_statuses checks message counts across the same selected accounts. These multi-inbox tools are read-only; archive, move, trash, delete, and mark operations remain scoped to one explicit account.

Search filters can be combined and include from, to, cc, bcc, subject, body-only text, all header/body text, Message-ID, internal and sent dates, message size, IMAP keywords, and the seen, flagged, answered, draft, or deleted states. All supplied filters must match. Search summaries include messageId, inReplyTo, references, and a header-derived threadId. State filters use explicit three-way values rather than checkboxes: for example, seen accepts any, seen, or unseen, while flagged accepts any, flagged, or unflagged. The answered, draft, and deleted filters follow the same pattern, so an unset state is distinct from not filtering by that state.

email_get_message_thread accepts the folder and IMAP UID of any message and returns up to 100 summaries from the same header-based conversation, oldest first. This works without requiring the optional IMAP THREAD extension, but it is scoped to one folder and depends on messages having valid Message-ID, In-Reply-To, and References headers. Use each returned IMAP UID with email_get_message when full message content is required.

email_get_mailbox_status returns the total and unread message counts plus IMAP RECENT, UIDNEXT, and UIDVALIDITY values for one folder.

email_get_message includes a zero-based attachmentIndex for every attachment. Pass the folder, IMAP UID, and index to email_get_message_attachment to retrieve its raw bytes as contentBase64.

IMAP configuration is required for every account. SMTP is optional; accounts without it can read, search, draft, and manage mail but cannot use email_send_draft. email_list_accounts reports capabilities.canSend so MCP clients can determine whether sending is available before attempting it.

email_update_message_flags accepts one IMAP UID or an array of up to 100 IMAP UIDs and seen: true/false for read/unread and flagged: true/false for flagged/unflagged. Either or both states can be changed in one call.

email_move_messages accepts either one IMAP UID or an array of up to 100 IMAP UIDs and moves them in one IMAP operation to an explicit targetFolder.

Use email_archive_messages for archiving; it moves up to 100 messages to the account's advertised IMAP Archive folder, or the provider all-mail folder when that is the advertised archive destination.

Use email_move_messages_to_trash for normal deletion; it moves up to 100 messages to the folder advertised with the IMAP \Trash special-use flag. email_delete_messages_permanently permanently marks and expunges up to 100 messages by IMAP UID and should only be used when permanent deletion is intended.

Sending is a two-step workflow: call email_create_message_draft, then pass its returned folder and IMAP UID field uid to email_send_draft. After SMTP accepts the message, the server appends a copy to the IMAP Sent folder and then removes the draft. A Sent append failure is reported as sentSaved: false, and a cleanup failure is reported as draftDeleted: false, without reporting the already accepted send as failed.

Draft attachments use native MIME encoding. Pass up to 20 attachments as attachments, each with filename, contentType, and contentBase64. The base64 value must contain the raw file bytes without a data-URL prefix. Draft text and HTML bodies are emitted as quoted-printable MIME parts; base64 is only required for attachment bytes.

To create a reply, call email_create_message_draft with replyToMessage.folder and the original message's IMAP UID in replyToMessage.uid. The server derives the recipient and subject and adds the correct In-Reply-To and References headers. Reply drafts quote the original message by default; set replyToMessage.quoteOriginal: false only when the original message should not be quoted. Set replyAll within replyToMessage when needed; top-level to, cc, and subject values override the derived values. Set top-level replyTo when the outgoing draft should include a Reply-To header. email_create_forward_draft creates a forward draft and includes the original attachments unless includeAttachments is false. email_update_message_draft replaces an existing IMAP draft and returns its new IMAP UID; omitted fields are preserved, and an empty attachments array removes all attachments.

Provider

IMAP

SMTP

Authentication

Gmail

imap.gmail.com:993 TLS

smtp.gmail.com:465 TLS

Google app password

Outlook

outlook.office365.com:993 TLS

smtp.office365.com:587 STARTTLS

Microsoft OAuth2 sign-in

iCloud

imap.mail.me.com:993 TLS

smtp.mail.me.com:587 STARTTLS

Apple app-specific password

Yahoo

imap.mail.yahoo.com:993 TLS

smtp.mail.yahoo.com:465 TLS

Yahoo app password

Custom

Provider supplied

Provider supplied

Password or OAuth2

For port 587, set smtpSecure to false; the native SMTP client negotiates STARTTLS. Cloudflare blocks outbound SMTP port 25.

Outlook refresh uses the Entra application client ID and Worker-held client secret. Microsoft may require tenant administrator consent for delegated Exchange permissions, depending on the tenant's consent policy. SMTP AUTH must also be enabled for the Exchange Online mailbox if sending fails.

Security

Cloudflare Access authenticates users and enforces the application policy before traffic reaches the Worker. The Worker independently validates the Access JWT using the team JWKS, issuer, and application audience. Mailbox passwords and OAuth tokens are AES-256-GCM encrypted in Workers KV and are never returned by account tools. Changing CREDENTIAL_ENCRYPTION_KEY makes existing stored accounts unreadable.

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    D
    maintenance
    Enables reading and sending emails via IMAP and SMTP through the MCP protocol. Supports multiple email accounts and configuration via UI or environment variables.
    BSD 3-Clause
  • A
    license
    B
    quality
    B
    maintenance
    Enables users to manage email accounts via IMAP/SMTP, including reading, searching, sending emails with attachments and calendar invites, all through natural language interactions with MCP-compatible clients.
    1
    4
    MIT
  • A
    license
    A
    quality
    D
    maintenance
    Enables Gmail management via MCP, including email search, retrieval, labeling, sending, and forwarding through IMAP and SMTP.
    15
    11
    BSD 3-Clause
  • A
    license
    A
    quality
    B
    maintenance
    MCP server that enables email management (send, read, search, delete, etc.) via IMAP/SMTP, compatible with Gmail, Outlook, Yahoo, iCloud, and other standard mail servers.
    11
    MIT