outlook-mcp
outlook-ews-mcp is an MCP server that connects compatible clients to on-prem Microsoft Exchange/EWS so they can read and manage mailbox data.
Email: list, search, read, send, reply, forward, move, copy, delete, mark, categorize, bulk actions, drafts, raw MIME export, and attachment add/delete/download.
Calendar: list, create, update, delete events, respond to invites, find free slots, check availability, view shared/delegate calendars, Room Finder, and bulk actions.
Contacts: search, read, create, update, and delete personal contacts.
Folders and attachments: list/create/rename/delete folders and save attachments to disk.
System: check Exchange connectivity, get mailbox metadata, list delegates, manage inbox rules, and get/set out-of-office replies.
Operations: NTLM/Basic auth over stdio, Streamable HTTP, or SSE; centralized error mapping and a shared request queue with concurrent read-only calls.
The server is distributed as a PyPI package named outlook-ews-mcp.
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., "@outlook-mcp find my unread emails from last week"
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.
outlook-ews-mcp
outlook-ews-mcp is an MCP server for on-prem Microsoft
Exchange via EWS (exchangelib). It gives
MCP-compatible clients (Claude Desktop, Claude Code, and any other MCP client) access to
email, calendar, contacts, folders, attachments, and availability data through a single,
testable Python service — no direct mailbox scripting required.
Renamed from
outlook-mcp. That name was already taken on PyPI by an unrelated project, so the distribution and CLI name are nowoutlook-ews-mcp. The Python import path is unchanged. Until the first tagged PyPI release, install from this repository as shown below.
Contents
Related MCP server: ews-outlook-mcp
Highlights
Email — list, search (substring or Advanced Query Syntax), read, send, reply, forward, move, copy, delete, mark, categorize, bulk actions, raw MIME export, attachment add/delete
System — Inbox Rules, Out-of-Office (automatic replies), read-only delegate listing
Calendar — list, create, update, delete, respond to invites, find free slots, view a shared/delegate mailbox's calendar, Room Finder, bulk actions
Contacts — search, read, create, update, delete
Folders & attachments — folder CRUD and attachment download
Auth —
NTLMandBasicagainst on-prem ExchangeTransport —
stdio, Streamable HTTP and (deprecated) SSE; HTTP requires a bearer tokenArchitecture — centralized error mapping through a single
ExchangeClientabstraction (see Project notes)Safety — a privacy-safer smoke check by default (see Smoke check)
Ops — Docker image plus GitHub and GitLab CI/CD pipelines included
Tool catalog
Every tool below is registered in tool_specs.py, the
single source of truth for its name, description, and schema. Read-only marks tools
that never modify the mailbox — they get more concurrency (see
Request queue) and are safe to call speculatively.
System
Tool | Description | Read-only |
| Check connectivity to Exchange | ✅ |
| Get mailbox metadata | ✅ |
| List mailbox delegates and their folder permission levels — read-only because | ✅ |
| List server-side inbox rules | ✅ |
| Create a server-side inbox rule, e.g. "from this sender → move to folder" | |
| Enable/disable a rule or change its priority (other fields aren't updatable here) | |
| Delete a server-side inbox rule by id | |
| Get the out-of-office (automatic reply) settings | ✅ |
| Turn automatic replies off, on, or schedule a start/end window |
⚠️
create_inbox_rule/update_inbox_rule/delete_inbox_rulemanage rules over EWS, which removes the client-side rule blob desktop Outlook keeps — this can wipe rules a user created in Outlook itself. This is documented EWS behavior, not a bug here.
Tool | Description | Read-only |
| List emails in a folder; | ✅ |
| Get a full email by id | ✅ |
| Export a message's raw RFC 822 MIME content, base64-encoded | ✅ |
| Get every message of a conversation in order, bodies included | ✅ |
| Search by substring (subject/body/sender) or server-side Advanced Query Syntax; | ✅ |
| Send a new email; | |
| Reply to an email | |
| Forward an email | |
| Move an email to another folder | |
| Copy an email to another folder | |
| Bulk move, with per-item results — one bad id doesn't fail the rest | |
| Bulk copy, with per-item results | |
| Bulk delete, with per-item results (soft-deletes unless | |
| Delete an email | |
| Update read state, importance, or the follow-up flag | |
| Set, add, or remove Outlook categories (the coloured labels) | |
| Bulk version of | |
| Bulk version of | |
| List categories in use with counts, sampled from recent messages (not the mailbox master category list) | ✅ |
| List mailbox folders | ✅ |
| Create a mailbox folder | |
| Rename a folder — refuses built-in folders (Inbox, Sent Items, Calendar, ...) | |
| Delete a folder and everything in it — refuses built-in folders | |
| Create an email draft | |
| Create a reply-all draft in the original conversation, with additional recipients, mentions and attachments; does not send | |
| Update a draft; omitted fields are left unchanged, | |
| Submit a draft and confirm its copy in Sent Items | |
| Attach a local file to a message, typically a draft — the file must live under | |
| Remove one attachment from a message by id | |
| Save an attachment to disk |
Calendar
Tool | Description | Read-only |
| List calendar events in a time range; pass | ✅ |
| Get a calendar event by id; pass | ✅ |
| Create a calendar event | |
| Update a calendar event | |
| Delete a calendar event | |
| Accept, decline, or tentatively respond to an invite | |
| Find open meeting time slots | ✅ |
| Bulk delete events, with per-item results | |
| Bulk respond to invites, with per-item results | |
| Get free/busy slots; pass | ✅ |
| List calendars | ✅ |
| List Room Finder room lists (groups of meeting rooms) | ✅ |
| List the meeting rooms in a Room Finder room list | ✅ |
Contacts
Tool | Description | Read-only |
| Search contacts | ✅ |
| Get a contact by id | ✅ |
| Create a personal contact | |
| Update a personal contact | |
| Delete a personal contact |
Typical use cases
Connect Claude Desktop or another MCP client to on-prem Exchange
Search inbox messages and fetch full email content
Send or draft emails from AI workflows
Inspect calendars and create meetings
Check free/busy windows for scheduling
Search personal contacts or the Global Address List
Expose Exchange operations through a controlled MCP boundary instead of direct mailbox scripting
Security notes
What the current code does:
Scoped connectivity | Connects only to the Exchange/EWS endpoint configured in |
No telemetry | Contains no telemetry, analytics, or third-party data export logic |
Secrets stay local | Keeps secrets in environment variables / |
Clean error payloads | Structured MCP error responses never include raw Exchange exception text, message bodies, attachment contents, or passwords; successful tools return only the mailbox data they were asked for |
Clean logs |
|
Clean Docker builds |
|
What you should still be careful with:
EXCHANGE_VERIFY_SSL=falsedisables TLS certificate verification — trusted internal/self-signed environments only. PreferEXCHANGE_CA_BUNDLE, which keeps verification on.EXCHANGE_AUTH_TYPE=Basicsends credentials in the clear, so the server refuses to start against anhttp://EXCHANGE_SERVER; only override withEXCHANGE_ALLOW_INSECURE_BASIC_AUTH=truefor a local/test server you control.get_attachmentwrites files to disk, andsend_email/reply_email/forward_email/create_draftread local files (viaattachments) and attach their contents to outgoing mail. Combined with untrusted email content, this is a plausible path for prompt-injected exfiltration of any file readable by the process. Local file access is refused by default and only works onceEXCHANGE_ATTACHMENT_ROOTis set to an absolute directory, which then confines bothattachmentspaths andget_attachment'ssave_pathto that directory tree (an unsetsave_pathstill falls back to the system temp directory).outlook-ews-mcp-smokeis privacy-safe by default and prints only masked mailbox info plus counts; setOUTLOOK_MCP_SMOKE_INCLUDE_DATA=trueonly if you explicitly want real inbox/event data in stdout.If you enable file logging with
LOG_FILE, protect that file with OS permissions.If you publish Docker images from CI, protect GitLab/GitHub project access and registry permissions.
Quick start
git clone https://github.com/viartemev/outlook-ews-mcp
cd outlook-ews-mcp
uv venv
source .venv/bin/activate
uv pip install -e '.[dev]'
cp .env.example .env
outlook-ews-mcpBy default the server runs in stdio mode. Set MCP_TRANSPORT=streamable-http (or the
deprecated sse, for clients that don't support Streamable HTTP yet) to start an HTTP
server instead; the endpoint is /mcp (/sse for SSE). HTTP mode requires
MCP_HTTP_AUTH_TOKEN, which clients send as Authorization: Bearer <token> — every
request without it gets 401. The token is a static shared secret, not per-user auth:
anyone holding it gets full access to the mailbox (read, send, delete), so keep TLS
termination (and ideally an IP allowlist) in front of the server when it leaves loopback.
Configuration
Minimal .env to get started — everything else below has a working default:
EXCHANGE_SERVER=https://mail.company.com/EWS/Exchange.asmx
EXCHANGE_USERNAME=DOMAIN\username
EXCHANGE_PASSWORD=secret
EXCHANGE_EMAIL_ADDRESS=user@company.com
EXCHANGE_AUTH_TYPE=NTLMA fully commented copy of every variable lives in .env.example.
Variable | Default | Description |
| (required) | EWS endpoint URL, e.g. |
| (required) |
|
| (required) | Account password |
| unset | SMTP address; set when |
|
|
|
|
| Allow |
|
| Verify the server's TLS certificate; |
| unset (OS trust store) | PEM file to verify the server certificate against instead of the OS trust store |
| unset (auto-detected) | Exchange server version, e.g. |
|
| Used only when Exchange reports an unresolvable GUID timezone id; normal operations use the mailbox's own default timezone |
|
| Per-request timeout in seconds (1–300) |
|
| Wall-clock retry budget for read-only calls when Exchange reports itself busy, not a retry count; |
| unset | Mailbox to impersonate (requires Exchange impersonation permissions) |
|
| Max size per attachment, enforced on both upload and |
|
| Max attachments on a single send/reply/forward/create_draft call (1–100) |
|
| Max combined attachment size on a single call (1–500) |
| unset (disabled) | Directory that confines attachment paths. Unset refuses all local file access for |
|
| Cap on |
|
| Cap on raw MIME export size before base64 expansion (1–100) |
| unset | Appended to outgoing text bodies and replies/forwards. No EWS signature API exists, so this is configuration, not the mailbox's Outlook signature |
| unset | Appended to outgoing HTML bodies. Same caveat as above; no cross-conversion between the two. Either can be skipped per call with |
|
|
|
|
| Bind host for the HTTP transports |
|
| Bind port for the HTTP transports |
| — | Required for the HTTP transports: bearer token clients must send (e.g. |
|
| Concurrent read-only tool calls (1–8); mutating calls always run exclusively. See Request queue |
|
| Max calls admitted at once, running + waiting (1–1000); beyond that, calls get an immediate |
|
|
|
| unset (stderr) | Log file path; protect it with OS permissions if set |
Behavior notes that aren't tied to a single variable:
list_eventsandfind_free_slotsaccept a boundedlimit(default 200, maximum 1000); event ranges are capped at 366 days and free-slot ranges at 31 days, so broad queries can't produce unbounded EWS or MCP responses.Listings stay lean by design: email summaries carry the sender but not recipient lists (
get_emailhas them),list_eventsreturns events without bodies (get_eventhas them), andget_emailreturns RFC-822 headers only withinclude_headers: true.Direct sends report
submitted, not recipient delivery.send_draftlooks up the sent copy and returns its new ID when confirmed; it never echoes the invalidated draft ID.Attachment metadata includes
downloadable; embedded Exchange item attachments havedownloadable: falseand can't be saved byget_attachment.
Reply drafts, Outlook mentions, and send confirmation
Use create_reply_draft to prepare an actual EWS reply for review. It defaults to
reply_all: true, keeps the source conversation and reply references, and accepts
additional_to, additional_cc and attachments. Call send_draft only after
review. reply_email still sends immediately; it also accepts HTML and mentions.
For send_email, create_draft, reply_email, create_reply_draft and
update_draft, use explicit mention tokens and metadata:
{
"id": "<source-message-id>",
"reply_all": true,
"body": "{{mention:owner}}, please prepare the equipment for collection.",
"mentions": [
{"key": "owner", "email": "owner@example.com", "display_name": "Equipment owner"}
],
"additional_cc": ["logistics@example.com"],
"attachments": ["/configured-attachment-root/collection.xlsx"]
}Mention tokens must appear in body text, not HTML attributes, links, scripts or
styles. The server escapes plain text and display names, generates unique
OWAAM... HTML anchors, and writes the X-Mentions Internet header through an
EWS extended property. Both the anchor and header are required for Outlook's
mention indicator; a literal @name or a mailto link alone is insufficient.
Mentioned addresses absent from To/CC are added to To; existing CC recipients
stay in CC. Bodies with mentions use HTML and the configured HTML signature.
This is an Outlook client convention, verified with Exchange 2016 and Legacy
Outlook for Mac; other client/server combinations require their own validation.
For partial updates, omitted fields remain unchanged, explicit null clears
nullable fields, and attachments: [] removes attachments. Supplying mentions
requires a replacement body. Replacing the body without mentions clears old
mention notification metadata. The MCP adapter preserves omission before request
validation, including for contacts and calendar updates.
send_draft stamps a unique submission_id, calls SendItem exactly once with
the copy destination explicitly set to this account's Sent Items, then polls for
the saved copy. confirmation_timeout_seconds defaults to 10, accepts 0–30,
and 0 skips polling. The budget limits polling; a final network request can still
take up to the configured Exchange request timeout.
sent_confirmed: the non-draft copy was found;idis its new Exchange ID.submitted: Exchange accepted SendItem; confirmation was disabled.submitted_unconfirmed: Exchange accepted SendItem, but the read failed or the copy was not visible before the polling budget expired. Do not resend.send_outcome_unknownerror: SendItem failed or its response was lost. Do not automatically retry; inspect Sent Items before another attempt.
Results include the actual sent_copy_mailbox, sent_copy_folder and, when
available, datetime_sent. A Sent Items copy confirms submission, not recipient
delivery or refresh of Outlook's local cache. Operators can correlate uncertain
sends using the named String property OutlookMcpSubmissionId in property set
a8b318c0-6eae-4c97-91aa-5ef142043c2b. Unit tests never send real mail.
Request queue
Clients issue several tool calls in parallel. Exchange work is blocking, so the server runs it in worker threads and admits calls through one shared FIFO queue.
MCP_MAX_CONCURRENCY(default4) sets how many read-only calls run at once, so an agent asking for an email, the folder list, and the calendar pays the slowest round trip instead of the sum. Mutating calls always run exclusively — one at a time, never overlapping a read — so read/write races on shared account state can't happen. Callers beyond the limit wait their turn, served in arrival order; a waiting mutation blocks later reads from overtaking it.MCP_MAX_QUEUE_SIZE(default20) caps how many calls can be admitted at once, running or waiting. Once that many are already in, further calls get an immediateserver_busyerror instead of joining an unbounded queue.The transport stays responsive while work is in flight. Tools are awaited rather than run on the event loop thread, so finished responses go out immediately and pings are answered while a long call is still running.
There is no per-call timeout, deliberately. A thread blocked on a socket read can't be killed from outside; the runtime can only stop waiting for it, which abandons the thread along with the EWS session it holds.
exchangelib's session pool has a hard maximum and hands out sessions in a loop with no give-up path, so leaked sessions eventually starve it and every later call blocks forever. A slow call is waited out instead, bounded byEXCHANGE_TIMEOUTplusEXCHANGE_MAX_RETRY_WAIT_SECONDS: the account's retry policy is fail-fast, so every EWS call raises on its first transient error rather thanexchangelibretrying it forever internally, andExchangeClientretries only read-only calls itself, bounded by that wall-clock budget. Writes are never auto-retried. Overruns past the expected budget are logged.
Claude Desktop / Claude Code example
GUI clients don't see the virtualenv's bin directory, so use the absolute path to the
installed script:
{
"mcpServers": {
"outlook": {
"command": "/path/to/outlook-ews-mcp/.venv/bin/outlook-ews-mcp",
"env": {
"EXCHANGE_SERVER": "https://mail.company.com/EWS/Exchange.asmx",
"EXCHANGE_USERNAME": "DOMAIN\\username",
"EXCHANGE_PASSWORD": "secret",
"EXCHANGE_EMAIL_ADDRESS": "user@company.com",
"EXCHANGE_AUTH_TYPE": "NTLM"
}
}
}
}Claude Code (credentials are read from the repository's .env):
claude mcp add outlook -- sh -c 'cd /path/to/outlook-ews-mcp && exec .venv/bin/outlook-ews-mcp'Smoke check
After filling in .env, run:
outlook-ews-mcp-smokeDefault output is sanitized for safer verification. If you intentionally want sample mailbox/event data in the output:
OUTLOOK_MCP_SMOKE_INCLUDE_DATA=true outlook-ews-mcp-smokeDocker
docker build -t outlook-ews-mcp .
docker run --rm --env-file .env outlook-ews-mcpCI/CD
GitHub Actions and GitLab CI both run lint, formatting, type checks, tests, dependency
audit, and package builds, using the uv version pinned in pyproject.toml.
GitHub | Additionally publishes tagged releases ( |
GitLab | Additionally builds and pushes a Docker image to the GitLab Container Registry on the default branch and on tags, using the built-in |
Default image tagging behavior:
Trigger | Tags pushed |
Default branch |
|
Git tag |
|
Development
uv sync --extra dev
uv run ruff check .
uv run pytest -qProject notes
The implementation is centered around a single
ExchangeClientabstraction so auth, transport, retries, and error mapping stay centralized.Errors are returned in a structured JSON form suitable for MCP
isError=truehandling.
Contributing
Bug reports and PRs are welcome — see CONTRIBUTING.md for how to set up a dev environment and run the test suite without a real Exchange server. For vulnerability reports, see SECURITY.md.
License
MIT — see LICENSE.
Available Tools
31 toolscopy_emailC
Copy email to another folder
Inputs: kwargs (required). Returns: result
create_contactC
Create a personal contact
Inputs: kwargs (required). Returns: result
create_draftC
Create an email draft
Inputs: kwargs (required). Returns: result
create_eventC
Create a calendar event
Inputs: kwargs (required). Returns: result
create_folderC
Create a mailbox folder
Inputs: kwargs (required). Returns: result
delete_contactC
Delete a personal contact
Inputs: kwargs (required). Returns: result
delete_emailC
Delete an email
Inputs: kwargs (required). Returns: result
delete_eventC
Delete a calendar event
Inputs: kwargs (required). Returns: result
find_free_slotsD
Find meeting time slots
Inputs: kwargs (required). Returns: result
forward_emailC
Forward an email
Inputs: kwargs (required). Returns: result
get_attachmentC
Save an attachment to disk
Inputs: kwargs (required). Returns: result
get_contactC
Get a contact by ID
Inputs: kwargs (required). Returns: result
get_emailC
Get a full email by ID
Inputs: kwargs (required). Returns: result
get_eventB
Get a calendar event by ID
Inputs: kwargs (required). Returns: result
get_mailbox_infoC
Get mailbox metadata
Inputs: kwargs (required). Returns: result
get_my_availabilityC
Get free and busy slots
Inputs: kwargs (required). Returns: result
list_calendarsC
List calendars
Inputs: kwargs (required). Returns: result
list_emailsD
List emails in a folder
Inputs: kwargs (required). Returns: result
list_eventsC
List calendar events in a time range
Inputs: kwargs (required). Returns: result
list_foldersC
List mailbox folders
Inputs: kwargs (required). Returns: result
mark_emailC
Update email flags
Inputs: kwargs (required). Returns: result
move_emailC
Move email to another folder
Inputs: kwargs (required). Returns: result
ping_exchangeC
Check connectivity to Exchange
Inputs: kwargs (required). Returns: result
reply_emailC
Reply to an email
Inputs: kwargs (required). Returns: result
respond_to_inviteC
Respond to a calendar invite
Inputs: kwargs (required). Returns: result
search_contactsD
Search contacts
Inputs: kwargs (required). Returns: result
search_emailsD
Search emails
Inputs: kwargs (required). Returns: result
send_draftC
Send a draft email
Inputs: kwargs (required). Returns: result
send_emailC
Send a new email
Inputs: kwargs (required). Returns: result
update_contactC
Update a personal contact
Inputs: kwargs (required). Returns: result
update_eventC
Update a calendar event
Inputs: kwargs (required). Returns: result
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 31 tools
Most tools have distinct purposes, but `find_free_slots` and `get_my_availability` overlap in functionality (both deal with availability). Otherwise, email, calendar, and contact operations are clearly separated.
All tool names follow a consistent `verb_noun` pattern in snake_case (e.g., `create_event`, `list_emails`, `send_email`). No mixing of conventions.
With 31 tools, the server covers email, calendar, contacts, and folders comprehensively. However, the count is on the higher side for a single server, exceeding the typical 3–15 range, though still manageable.
The tool set provides CRUD for emails, events, contacts, and folders, plus search, send, reply, forward, and move. Minor gaps exist (e.g., no attachment creation, no folder update), but core workflows are well-covered.
Maintenance
Related MCP Connectors
Hosted MCP server for your Gmail, Outlook and IMAP mail, with mailboxes and scopes set per agent.
Hosted email MCP for your own Gmail, Outlook.com, Microsoft 365, iCloud or IMAP inbox: read, search, draft, reply in thread, forward and file mail. It moves or flags up to 500 messages in one call, and a send leaves exactly one copy in Sent. A calendar is a separate connection, and connecting one adds diary and scheduling tools.
Secure MCP server for exploring incwo CRM data, documents, and email workflows.
Screenshots, PDFs, image editing, QR codes, email validation and more through one hosted MCP server.
Related MCP Servers
- FlicenseAqualityDmaintenanceMCP server for corporate Exchange that provides access to email, calendar, and people directory via OWA JSON API.201-
- FlicenseAqualityDmaintenanceMCP server for Claude to access on-premises Outlook/Exchange mailboxes via EWS with NTLM authentication, providing tools for email, calendar, and contact management without relying on Microsoft 365 or Graph API.181-
- AlicenseAqualityCmaintenanceA local MCP server for on-premises Microsoft Exchange, connecting via EWS and NTLM. It provides mail, template, availability, and calendar workflow tools through stdio, with draft-first safety and Windows Credential Manager integration.118MIT
- -licenseNot gradedqualityDmaintenanceSecure MCP server for Microsoft Exchange. It enables authorized users to list, search, read, download attachments, send, and reply to email via EWS with strict access control and audit.-