Agent Email
MCP serverCommunicationThis 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
- Save this item in Your setup as a reference.
- Read the source or reference documentation for its setup requirements. Saving it here does not connect it to your AI.
- 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, andget_threadcreate_draft,update_draft,send_draft,send_email, andreply_to_emaillabel_email,mark_read, andmove_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
mcpconfig 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 withnpm install openclaw@latestin your NemoClaw directory, or restart the gateway directly withlaunchctl 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_statuslist_emails+read_emailmark_readunread -> read -> unreadlabel_emailon a safe inbox candidatecreate_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.
| Profile | Microsoft delegated scopes | Exposed tools |
|---|---|---|
observe | Mail.Read, User.Read, offline_access | Read-only email tools, minus list_inbox_rules (see below) |
full (default) | Mail.Read, Mail.ReadWrite, Mail.Send, MailboxSettings.ReadWrite, User.Read, offline_access | All 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:
| Tool | Description | Type |
|---|---|---|
list_emails | List recent emails with filtering | read |
read_email | Read full email content as markdown, or raw HTML with format: "html" | read |
search_emails | Full-text search across mailboxes | read |
list_mailboxes | Enumerate configured mailboxes, their status, and the default | read |
get_mailbox_status | Connection status and warnings | read |
get_thread | Full conversation context | read |
list_attachments | List attachment metadata for an email | read |
download_attachment | Download a file attachment as base64 | read |
send_email | Send new email (allowlist-gated) | write |
reply_to_email | Reply within thread (allowlist-gated on send) | write |
create_draft | Create email draft | write |
update_draft | Update draft content | write |
send_draft | Send a saved draft | write |
list_scheduled_sends | List pending provider-held scheduled sends (Microsoft 365) | read |
cancel_scheduled_send | Cancel a pending scheduled send (Microsoft 365) | destructive |
label_email | Apply labels/categories | write |
flag_email | Flag/unflag emails | write |
mark_read | Mark as read/unread | write |
move_to_folder | Move between folders | write |
delete_email | Delete (requires operator env + caller flag) | destructive |
list_folders | Recursively list folders and computed paths (Microsoft 365) | read |
create_folder | Create a custom child folder (Microsoft 365) | write |
delete_folder | Delete a custom folder (requires operator env + caller flag); system folders are protected (Microsoft 365) | destructive |
list_inbox_rules | List server-side inbox rules (Microsoft 365) | read |
create_inbox_rule | Create a persistent safe inbox rule; forwarding, redirection, deletion, and discarding to Deleted Items are blocked (Microsoft 365) | write |
delete_inbox_rule | Delete 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:
format | Behavior |
|---|---|
markdown (default) | Rendered to HTML via marked (GFM, breaks: true). Raw HTML embedded in the markdown is preserved. |
html | Passthrough — your HTML is sent as-is. |
text | No 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, ortext. You gettextwhen you asked forhtmlbut the message has no HTML part, in which case the plain-text body is returned. Check this before writing a body back.bodyTruncated— present andtrueonly if the body exceeded the 256 KB response budget forformat: "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 pass | Resolves against | Finds a file in an allowed root? |
|---|---|---|
contract.pdf | EMAIL_MCP_SAFE_DIR (default: cwd) | ❌ relative paths never search the extra roots |
~/Downloads/contract.pdf | EMAIL_MCP_SAFE_DIR — the ~ is not expanded | ❌ |
/Users/you/Downloads/contract.pdf | each 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
| Provider | Status | Package |
|---|---|---|
| Microsoft 365 (Graph API) | Fully supported | @usejunior/provider-microsoft |
| Gmail | Supported 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.
| Path | Command | Google Cloud project needed? |
|---|---|---|
| Default OAuth client | email-agent-mcp configure --provider gmail | No |
| Bring your own key (BYOK) | same command plus --client-id / --client-secret | Yes, 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
github.com/usejunior/email-agent-mcp
More in Communication
MCP server · builderio
More in Communicationpalisade
MCP server · palisadeemail
More in Communicationagent-native-brain
MCP server · builderio
More in Communicationqr-generator
MCP server · modelcontextprotocol
More in Communicationshipmail-mcp
MCP server · shipmail-to
More in Communicationagentphone
MCP server · agentphone-ai
More in Communication