Agent Email

MCP serverCommunication

This integration gives your AI direct access to your Outlook email. Once added, your AI can read incoming mail, write drafts, send messages, and keep your inbox organized on your behalf.

Unavailable. This server has no hosted endpoint yet, so Ahel can't serve it.

Add to setup to save this item as a reference. Ahel cannot run it, and signing in will not install it.

What your AI can do with it

  • Read messages in your Outlook inbox
  • Draft replies and new emails for your review
  • Send email from your Outlook account
  • Organize mail by sorting and filing messages
  • Work with your email locally on your own machine

Getting started

  1. Save this item in Your setup as a reference.
  2. Read the source or reference documentation for its setup requirements. Saving it here does not connect it to your AI.
  3. Check this page for availability before trying to install it through Ahel.

From the project's README

As published by usejunior/email-agent-mcp in README.md.

English | Español | 简体中文 | Português (Brasil) | Deutsch

email-agent-mcp by UseJunior -- local email connectivity for AI agents.

Agent Email is an open-source TypeScript MCP server that lets Claude Code, Cursor, Gemini CLI, OpenClaw, and other MCP-compatible runtimes read email, search threads, draft replies, label messages, change read state, move messages, and send mail through your own mailbox. Microsoft 365 / Outlook and Gmail are supported today. Security-first defaults mean agents cannot send email until you explicitly configure an allowlist.

Quick Start

npx -y email-agent-mcp

The interactive setup wizard walks you through OAuth configuration and mailbox selection.

What Works Today

  • Microsoft 365 / Outlook mailbox access through MCP stdio
  • list_emails, read_email, search_emails, and get_thread
  • create_draft, update_draft, send_draft, send_email, and reply_to_email
  • label_email, mark_read, and move_to_folder
  • send allowlists, delete disabled by default, and sanitized errors

The current launch-prep pass was validated against a real Outlook mailbox for read, draft, send, categorize, move, and read-state flows.

Why This Exists

AI agents need to read, reply to, and act on email, but email APIs are complex. OAuth flows, Graph delta queries, Gmail push subscriptions, HTML-to-markdown conversion, threading semantics -- each provider has its own quirks.

Agent Email wraps this complexity into deterministic MCP tools with security guardrails:

  • send and receive allowlists that control who agents can contact
  • delete disabled by default (requires explicit opt-in)
  • error sanitization that strips API keys, file paths, and stack traces
  • body file sandboxing with path traversal protection

Use with Claude Code

Add to ~/.claude/settings.json or your project .claude/settings.json:

{
  "mcpServers": {
    "email-agent-mcp": {
      "type": "stdio",
      "command": "npx",
      "args": ["-y", "email-agent-mcp"]
    }
  }
}

Use with Cursor

// .cursor/mcp.json
{
  "mcpServers": {
    "email-agent-mcp": {
      "command": "npx",
      "args": ["-y", "email-agent-mcp"]
    }
  }
}

Use with Gemini CLI

gemini extensions install https://github.com/UseJunior/email-agent-mcp

Use with OpenClaw

Add an mcp block to ~/.openclaw/openclaw.json:

{
  // ... existing config ...
  mcp: {
    servers: {
      email: {
        command: "npx",
        args: ["tsx", "/path/to/email-agent-mcp/packages/email-mcp/src/serve-entry.ts"],
        transport: "stdio"
      }
    }
  }
}

Version note: The mcp config key requires OpenClaw app >= 2026.3.24. If the CLI is older than the app, it may reject this key during validation even though the gateway accepts it. Update the CLI with npm install openclaw@latest in your NemoClaw directory, or restart the gateway directly with launchctl kickstart -k gui/501/ai.openclaw.gateway.

Email watcher

The watcher polls your mailbox and sends wake signals to OpenClaw when new email arrives:

# Set the hooks token (must match hooks.token in openclaw.json)
export OPENCLAW_HOOKS_TOKEN="your-hooks-token"

# Start the watcher (defaults to http://localhost:18789/hooks/wake)
npm run dev:watch

The watcher requires at least one configured mailbox. Run npx email-agent-mcp or npm run dev:configure first to complete the OAuth flow.

Launch Prep Smoke Test

Before recording a demo, run the live smoke script against a real mailbox and a safe send allowlist. The script exercises:

  • get_mailbox_status
  • list_emails + read_email
  • mark_read unread -> read -> unread
  • label_email on a safe inbox candidate
  • create_draft
  • draft-only reply_to_email
  • optional send_email

Example:

EMAIL_AGENT_MCP_HOME=/tmp/email-agent-mcp-live \
AGENT_EMAIL_SEND_ALLOWLIST=/tmp/email-agent-mcp-live/send-allowlist.json \
npm run launch:prep:smoke -- --live-write --send-to beta@usejunior.com

Default safe-candidate selection looks for notifications@github.com in the inbox so you can rehearse the recording flow on a public-safe message instead of customer mail. If your mailbox status name is not an email address, pass --reply-sender <email> or set EMAIL_AGENT_MCP_REPLY_SENDER so the script can find a self-sent message for the draft-reply check.

Tool Reference

Scope profiles

Set EMAIL_AGENT_MCP_SCOPE_PROFILE before configuring and starting the server to choose the permissions exposed to an agent. The default is full for backward compatibility.

ProfileMicrosoft delegated scopesExposed tools
observeMail.Read, User.Read, offline_accessRead-only email tools, minus list_inbox_rules (see below)
full (default)Mail.Read, Mail.ReadWrite, Mail.Send, MailboxSettings.ReadWrite, User.Read, offline_accessAll tools

For an observation-only deployment, set the profile for both configuration and runtime so the OAuth consent and MCP tool list agree:

export EMAIL_AGENT_MCP_SCOPE_PROFILE=observe
npx email-agent-mcp configure
npx email-agent-mcp serve

observe's scopes are a deliberate strict subset of full's. That is what makes the profile adoptable: a tenant that has already consented to the full set grants observe silently, so switching full → observe needs no new consent. Switching observe → full does require a new interactive consent, because it asks for scopes that were never granted.

For the same reason observe does not expose list_inbox_rules. Graph gates /mailFolders/inbox/messageRules behind MailboxSettings, and Entra treats MailboxSettings.Read as a distinct scope from full's MailboxSettings.ReadWrite rather than implied by it. Requesting it would break the subset property and force a fresh consent on every observe deployment — an admin-approval request in tenants that restrict user consent. Use full if you need inbox-rule visibility.

An invalid profile value stops startup instead of silently granting broader access. If cached credentials do not cover the profile's scopes, the server fails fast with an actionable error rather than blocking on an interactive sign-in.

What observe does and does not guarantee

observe always removes the write tools from the MCP tool surface, so an agent cannot invoke them. That part holds everywhere.

It only narrows the OAuth token on a mailbox that has not already consented to the write scopes. Entra issues an access token carrying every scope the user or tenant has already consented to for that application — not just the subset requested at token-acquisition time. So if you point observe at a mailbox previously configured as full, the underlying token still carries Mail.ReadWrite and Mail.Send; only the tool surface is reduced.

For a genuine least-privilege token, consent to observe from a mailbox that has never been granted the write scopes — a fresh configure against an app registration whose delegated permissions stop at Mail.Read/User.Read. Treat the tool-surface reduction as defense in depth, not as an OAuth boundary, unless you control the grant.

Tools

The full profile exposes 26 MCP tools; observe omits every tool whose action is marked as mailbox-mutating:

ToolDescriptionType
list_emailsList recent emails with filteringread
read_emailRead full email content as markdown, or raw HTML with format: "html"read
search_emailsFull-text search across mailboxesread
list_mailboxesEnumerate configured mailboxes, their status, and the defaultread
get_mailbox_statusConnection status and warningsread
get_threadFull conversation contextread
list_attachmentsList attachment metadata for an emailread
download_attachmentDownload a file attachment as base64read
send_emailSend new email (allowlist-gated)write
reply_to_emailReply within thread (allowlist-gated on send)write
create_draftCreate email draftwrite
update_draftUpdate draft contentwrite
send_draftSend a saved draftwrite
list_scheduled_sendsList pending provider-held scheduled sends (Microsoft 365)read
cancel_scheduled_sendCancel a pending scheduled send (Microsoft 365)destructive
label_emailApply labels/categorieswrite
flag_emailFlag/unflag emailswrite
mark_readMark as read/unreadwrite
move_to_folderMove between folderswrite
delete_emailDelete (requires operator env + caller flag)destructive
list_foldersRecursively list folders and computed paths (Microsoft 365)read
create_folderCreate a custom child folder (Microsoft 365)write
delete_folderDelete a custom folder (requires operator env + caller flag); system folders are protected (Microsoft 365)destructive
list_inbox_rulesList server-side inbox rules (Microsoft 365)read
create_inbox_ruleCreate a persistent safe inbox rule; forwarding, redirection, deletion, and discarding to Deleted Items are blocked (Microsoft 365)write
delete_inbox_ruleDelete a server-side inbox rule (requires operator env + caller flag) (Microsoft 365)destructive

This is a shared cross-provider tool surface. Although label_email, flag_email, mark_read, move_to_folder, and delete_email are advertised, the Gmail adapter intentionally does not implement those mutation capabilities. They fail closed before any Gmail mutation request: unsupported actions return NOT_SUPPORTED, while deletion can stop first at the default DELETE_DISABLED operator gate. The default gmail.readonly plus gmail.compose grant would not authorize those mutations either; do not add gmail.modify to try to make the tools available. They remain available only for providers whose adapters and grants support them.

Every row returned by list_emails, search_emails, and get_thread, and the read_email response, carries an always-present isDraft boolean. isDraft: true means the message is an unsent draft: it has not been sent, and its receivedAt is provider-supplied metadata rather than evidence of delivery. The field is never omitted, so a consuming agent can distinguish "not a draft" from "draft status not reported" — but note false asserts only that the provider did not mark the message as an unsent draft, not that the mailbox owner sent it (received mail is also false). Drafts otherwise appear in listings and search results as before.

Folder and inbox-rule management requires Microsoft Graph MailboxSettings.ReadWrite consent. Existing Microsoft mailbox connections must re-consent after upgrading. Gmail uses labels rather than hierarchical folders/server-side Exchange rules, so these six tools return NOT_SUPPORTED for Gmail mailboxes.

Body formats

Bodies cross the wire as markdown by default in both directions. That default is deliberate — markdown is token-efficient, and a routine read should stay cheap. Both directions can opt out of it.

Writing — send_email, reply_to_email, create_draft, and update_draft accept an optional format:

formatBehavior
markdown (default)Rendered to HTML via marked (GFM, breaks: true). Raw HTML embedded in the markdown is preserved.
htmlPassthrough — your HTML is sent as-is.
textNo rendering; sent as plain text.

html is unsanitised passthrough: no sanitiser, no allowlist, no rewriting of your markup. Inline CSS and arbitrary tags survive to the wire — that is what makes styled mail possible, and it means you own whatever you send. The one default modification is the force-black wrapper below; with force_black: false the body passes through byte-for-byte.

For markdown and html the rendered HTML is wrapped in a <div style="color: #000000;"> so Outlook's dark mode does not turn the text white-on-white.

Body files — send_email, create_draft, and update_draft also accept body_file: a path to a .md, .html, or .txt file (read relative to EMAIL_MCP_SAFE_DIR, default the process working directory, plus any AGENT_EMAIL_ALLOWED_DIRS roots) used as the body instead of body. A .md body file may open with a YAML frontmatter block:

For update_draft, body and body_file are permitted only on a non-reply draft and require replace_body: true; the stored body is then replaced wholesale. Reply draft bodies cannot be edited safely because they include provider-assembled quoted history, so create a new draft instead. Subject, recipient, and attachment updates remain available on reply drafts.

---
to: alex@example.com
subject: Quarterly summary
format: html
force_black: false
---
<p>The body starts after the closing delimiter.</p>

Recognized keys are to, cc, subject, reply_to, draft, format, and force_black (draft: true turns a send_email call into a draft save). A frontmatter value overrides the same-named tool parameter. Frontmatter is parsed only from .md files, and the file extension never selects the format — without an explicit format tool parameter, even a .html body file gets the default markdown rendering; pass format: "html" for passthrough.

Reading — read_email accepts an optional format of markdown (default) or html:

{ "id": "AAMkAD...", "format": "html" }

format: "html" returns the message's raw body HTML verbatim. Reach for it when you need styling that markdown cannot carry — color, background-color, text-decoration, <u> — for example to change one sentence of a formatted body and leave the rest alone. Through the markdown path that styling is silently destroyed on the way out.

Two fields come back with it:

  • bodyFormat — always present: markdown, html, or text. You get text when you asked for html but the message has no HTML part, in which case the plain-text body is returned. Check this before writing a body back.
  • bodyTruncated — present and true only if the body exceeded the 256 KB response budget for format: "html" (the markdown path is unbounded, as before). Do not write a truncated body back to a draft.

Raw HTML costs far more tokens than its markdown reduction, so leave the default alone unless you need the styling. strip_quoted_history and strip_signatures are markdown-shaped text transforms and are not applied when format is html — the raw HTML is returned untouched.

Writing it back. Pass format: "html" and force_black: false:

{ "draft_id": "AAMkAD...", "body": "<edited html>", "format": "html", "force_black": false }

force_black defaults to true, which wraps whatever HTML you send in a <div style="color: #000000;">. That is right for HTML you authored yourself, but on a body you just read back it nests one more wrapper on every cycle — after fifteen revisions you have fifteen nested divs. With force_black: false the bytes survive the round trip untouched.

Scheduled send

send_email and send_draft accept scheduled_send_at, an ISO 8601 future timestamp with an explicit timezone. Microsoft 365 holds the message server-side, so delivery survives this process exiting. Use the returned messageId with cancel_scheduled_send while it is pending, or inspect pending items with list_scheduled_sends. That listing covers messages you scheduled from Outlook itself, not just ones scheduled through this server — Outlook parks the held message wherever it likes (often Deleted Items), so discovery scans the mailbox rather than a folder, and reports only sends whose time is still ahead.

Microsoft Graph changes the message ID when the held draft moves to Sent Items, so the returned ID is a pre-delivery management handle, not a permanent sent message ID. Gmail's public API does not expose scheduled send; all scheduled send surfaces return NOT_SUPPORTED for Gmail while immediate sends remain unchanged.

Outbound attachments

send_email, reply_to_email, create_draft, and update_draft accept an optional attachments array. Each entry takes a sandboxed file path (read relative to EMAIL_MCP_SAFE_DIR, default the process working directory, plus any AGENT_EMAIL_ALLOWED_DIRS roots — see below) or inline base64, plus optional filename / mimeType overrides:

{
  "to": "alice@example.com",
  "subject": "Signed agreement",
  "body": "Attached as requested.",
  "attachments": [
    { "path": "./out/agreement.pdf" },
    { "base64": "iVBORw0KGgo...", "filename": "screenshot.png" }
  ]
}

Files are capped at 25MB each; Microsoft Graph additionally caps inline sends at ~3MB total (larger files need an upload session — not yet supported). For update_draft, omitting attachments preserves the draft's existing files; passing an array (even empty) replaces them.

Attaching files from outside the working directory — set AGENT_EMAIL_ALLOWED_DIRS to a delimiter-separated list of absolute directories (: on macOS/Linux, ; on Windows; a leading ~ is expanded) that attachments[].path and body_file may also read from:

{
  "mcpServers": {
    "email-agent-mcp": {
      "command": "npx",
      "args": ["-y", "@usejunior/email-agent-mcp"],
      "env": { "AGENT_EMAIL_ALLOWED_DIRS": "~/Downloads:/Volumes/Shared/Contracts" }
    }
  }
}

How a caller's path is resolved — the two rules below both surface as FILE_NOT_FOUND, which is easy to misread as a missing file:

Path you passResolves againstFinds a file in an allowed root?
contract.pdfEMAIL_MCP_SAFE_DIR (default: cwd)❌ relative paths never search the extra roots
~/Downloads/contract.pdfEMAIL_MCP_SAFE_DIR — the ~ is not expanded❌
/Users/you/Downloads/contract.pdfeach root in turn✅

The ~ shorthand is expanded in AGENT_EMAIL_ALLOWED_DIRS (the operator's config) but not in attachments[].path or body_file (the caller's argument) — pass those as absolute paths. And a relative path is deliberately confined to the safe directory: searching every allowed root for a bare contract.pdf would silently attach whichever copy happened to exist first.

Each root is canonicalised with realpath before the containment check: an allowlisted root that is itself a symlink resolves to its real location, a root that cannot be canonicalised authorises nothing, and a symlink escaping every root is still rejected. Unset (the default) keeps the working directory as the only root — which is why an agent that cannot reach a file should ask you to allowlist its directory rather than copy confidential documents into a git working tree.

Allowlisting a directory trusts everyone who can write to it. Validation and open are separate operations on a path, so a principal who can replace a directory inside an allowed root between the two can still redirect the read; only allowlist roots whose ancestors are not writable by untrusted users. The final file is opened with O_NOFOLLOW, so the file itself cannot be swapped for a symlink after validation.

Provider Support

ProviderStatusPackage
Microsoft 365 (Graph API)Fully supported@usejunior/provider-microsoft
GmailSupported via interactive CLI OAuth (default client or your own) or manual refresh-token setup@usejunior/provider-gmail

Use email-agent-mcp configure --provider gmail to run the local browser OAuth flow, or add a manual mailbox token file under ~/.email-agent-mcp/tokens/. See Gmail Setup below and packages/provider-gmail/README.md.

Gmail Setup

Gmail has two supported OAuth paths. Both end with the same result: a refresh token on your machine, and Gmail API calls going directly from your machine to Google.

PathCommandGoogle Cloud project needed?
Default OAuth clientemail-agent-mcp configure --provider gmailNo
Bring your own key (BYOK)same command plus --client-id / --client-secretYes, yours

Recommended for now: BYOK. Our default OAuth client is still in Google's "Testing" publishing status while verification is in progress, which caps it at 100 registered test users and shows the "Google hasn't verified this app" interstitial during consent. Verification for Gmail's restricted scope requires a CASA security assessment and takes several weeks; progress is tracked in issue #112. BYOK sidesteps the shared 100-user cap and puts the app's publishing status under your control; your own app still has Google's Testing constraints until you publish it.

Other reasons to pick BYOK: dedicated API quota, your own privacy policy and verification status, and no dependency on the hosted broker at https://oauth.usejunior.com.

Scopes requested

Agent Email requests exactly these two Gmail scopes by default:

https://www.googleapis.com/auth/gmail.readonly
https://www.googleapis.com/auth/gmail.compose

gmail.readonly is required for reading messages and threads; gmail.compose manages drafts and sending but does not authorize users.threads.get or users.messages.get. The default grant does not include gmail.modify. Separately, the Gmail adapter intentionally leaves label changes, read-state changes, moves, trash, and delete operations unsupported, so those tools fail closed with NOT_SUPPORTED. Add both scopes to your OAuth consent screen.

Deployments that explicitly set GMAIL_OAUTH_SCOPES must update it to the two space-separated scopes above. New authorizations and repeated authorizations must re-consent to the new scope pair. Existing mailbox files and their stored refresh-token metadata remain unchanged.

BYOK: create your own Google OAuth client

Shortened here. Read the whole README on GitHub.

Signals

GitHub stars
6
Forks
2
Last commit
Oct 2026
Weekly downloads
121
Weekly_downloads
470 weekly_downloads
Advanced
Delivery
email-agent-mcp MCP server → your Ahel connector (mcp.ahel.ai) → your AI.
Item type
mcp-server
Key
io-github-usejunior-email-agent-mcp
Source
github.com/usejunior/email-agent-mcp