Skip to main content
Glama

outlook-ews-mcp

Python MCP Exchange Status License

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 now outlook-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 — NTLM and Basic against on-prem Exchange

  • Transport — stdio, Streamable HTTP and (deprecated) SSE; HTTP requires a bearer token

  • Architecture — centralized error mapping through a single ExchangeClient abstraction (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

ping_exchange

Check connectivity to Exchange

✅

get_mailbox_info

Get mailbox metadata

✅

list_delegates

List mailbox delegates and their folder permission levels — read-only because exchangelib has no delegate write support

✅

list_inbox_rules

List server-side inbox rules

✅

create_inbox_rule

Create a server-side inbox rule, e.g. "from this sender → move to folder"

update_inbox_rule

Enable/disable a rule or change its priority (other fields aren't updatable here)

delete_inbox_rule

Delete a server-side inbox rule by id

get_out_of_office

Get the out-of-office (automatic reply) settings

✅

set_out_of_office

Turn automatic replies off, on, or schedule a start/end window

⚠️ create_inbox_rule / update_inbox_rule / delete_inbox_rule manage 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.

Email

Tool

Description

Read-only

list_emails

List emails in a folder; include_preview=true adds a 200-char body preview (downloads full bodies)

✅

get_email

Get a full email by id

✅

get_email_mime

Export a message's raw RFC 822 MIME content, base64-encoded

✅

get_thread

Get every message of a conversation in order, bodies included

✅

search_emails

Search by substring (subject/body/sender) or server-side Advanced Query Syntax; include_preview as in list_emails

✅

send_email

Send a new email; from_mailbox sends from a shared mailbox (needs Send As)

reply_email

Reply to an email

forward_email

Forward an email

move_email

Move an email to another folder

copy_email

Copy an email to another folder

move_emails

Bulk move, with per-item results — one bad id doesn't fail the rest

copy_emails

Bulk copy, with per-item results

delete_emails

Bulk delete, with per-item results (soft-deletes unless hard_delete)

delete_email

Delete an email

mark_email

Update read state, importance, or the follow-up flag

categorize_email

Set, add, or remove Outlook categories (the coloured labels)

mark_emails

Bulk version of mark_email, with per-item results

categorize_emails

Bulk version of categorize_email, with per-item results

list_categories

List categories in use with counts, sampled from recent messages (not the mailbox master category list)

✅

list_folders

List mailbox folders

✅

create_folder

Create a mailbox folder

rename_folder

Rename a folder — refuses built-in folders (Inbox, Sent Items, Calendar, ...)

delete_folder

Delete a folder and everything in it — refuses built-in folders

create_draft

Create an email draft

create_reply_draft

Create a reply-all draft in the original conversation, with additional recipients, mentions and attachments; does not send

update_draft

Update a draft; omitted fields are left unchanged, attachments (if given) replaces the whole set

send_draft

Submit a draft and confirm its copy in Sent Items

add_attachment

Attach a local file to a message, typically a draft — the file must live under EXCHANGE_ATTACHMENT_ROOT

delete_attachment

Remove one attachment from a message by id

get_attachment

Save an attachment to disk

Calendar

Tool

Description

Read-only

list_events

List calendar events in a time range; pass mailbox for a colleague's or shared mailbox's default calendar (needs read access to that calendar, not combinable with calendar_id). If only free/busy is shared, falls back to the availability service: events then have an empty id and carry only time, status, subject and location

✅

get_event

Get a calendar event by id; pass mailbox for a colleague's calendar

✅

create_event

Create a calendar event

update_event

Update a calendar event

delete_event

Delete a calendar event

respond_to_invite

Accept, decline, or tentatively respond to an invite

find_free_slots

Find open meeting time slots

✅

delete_events

Bulk delete events, with per-item results

respond_to_invites

Bulk respond to invites, with per-item results

get_my_availability

Get free/busy slots; pass mailbox for a colleague's calendar

✅

list_calendars

List calendars

✅

list_room_lists

List Room Finder room lists (groups of meeting rooms)

✅

list_rooms

List the meeting rooms in a Room Finder room list

✅

Contacts

Tool

Description

Read-only

search_contacts

Search contacts

✅

get_contact

Get a contact by id

✅

create_contact

Create a personal contact

update_contact

Update a personal contact

delete_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 EXCHANGE_SERVER

No telemetry

Contains no telemetry, analytics, or third-party data export logic

Secrets stay local

Keeps secrets in environment variables / .env, ignored by .gitignore (.env, .env.*, while keeping .env.example)

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

LOG_LEVEL only controls the app's own outlook_mcp.* loggers; exchangelib's SOAP XML loggers — which would otherwise dump full request/response XML, even at ERROR level on transport errors — are always force-silenced

Clean Docker builds

.dockerignore excludes .env, tests, caches, and VCS metadata from the build context

What you should still be careful with:

  • EXCHANGE_VERIFY_SSL=false disables TLS certificate verification — trusted internal/self-signed environments only. Prefer EXCHANGE_CA_BUNDLE, which keeps verification on.

  • EXCHANGE_AUTH_TYPE=Basic sends credentials in the clear, so the server refuses to start against an http:// EXCHANGE_SERVER; only override with EXCHANGE_ALLOW_INSECURE_BASIC_AUTH=true for a local/test server you control.

  • get_attachment writes files to disk, and send_email/reply_email/forward_email/ create_draft read local files (via attachments) 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 once EXCHANGE_ATTACHMENT_ROOT is set to an absolute directory, which then confines both attachments paths and get_attachment's save_path to that directory tree (an unset save_path still falls back to the system temp directory).

  • outlook-ews-mcp-smoke is privacy-safe by default and prints only masked mailbox info plus counts; set OUTLOOK_MCP_SMOKE_INCLUDE_DATA=true only 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-mcp

By 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=NTLM

A fully commented copy of every variable lives in .env.example.

Variable

Default

Description

EXCHANGE_SERVER

(required)

EWS endpoint URL, e.g. https://mail.company.com/EWS/Exchange.asmx

EXCHANGE_USERNAME

(required)

DOMAIN\username or a UPN. Exactly one backslash — dotenv does not process escape sequences

EXCHANGE_PASSWORD

(required)

Account password

EXCHANGE_EMAIL_ADDRESS

unset

SMTP address; set when EXCHANGE_USERNAME isn't one

EXCHANGE_AUTH_TYPE

NTLM

NTLM or Basic

EXCHANGE_ALLOW_INSECURE_BASIC_AUTH

false

Allow Basic auth over http:// — local/test servers only

EXCHANGE_VERIFY_SSL

true

Verify the server's TLS certificate; false only for trusted internal/self-signed setups

EXCHANGE_CA_BUNDLE

unset (OS trust store)

PEM file to verify the server certificate against instead of the OS trust store

EXCHANGE_VERSION

unset (auto-detected)

Exchange server version, e.g. EXCHANGE_2016

EXCHANGE_TIMEZONE_FALLBACK

Europe/Moscow

Used only when Exchange reports an unresolvable GUID timezone id; normal operations use the mailbox's own default timezone

EXCHANGE_TIMEOUT

30

Per-request timeout in seconds (1–300)

EXCHANGE_MAX_RETRY_WAIT_SECONDS

90

Wall-clock retry budget for read-only calls when Exchange reports itself busy, not a retry count; 0 disables retries. Writes are never auto-retried

EXCHANGE_IMPERSONATE_AS

unset

Mailbox to impersonate (requires Exchange impersonation permissions)

EXCHANGE_ATTACHMENT_MAX_SIZE_MB

10

Max size per attachment, enforced on both upload and get_attachment download (1–100)

EXCHANGE_ATTACHMENT_MAX_COUNT

10

Max attachments on a single send/reply/forward/create_draft call (1–100)

EXCHANGE_ATTACHMENT_MAX_TOTAL_SIZE_MB

25

Max combined attachment size on a single call (1–500)

EXCHANGE_ATTACHMENT_ROOT

unset (disabled)

Directory that confines attachment paths. Unset refuses all local file access for attachments/save_path; set to an absolute directory to allow paths inside it

EXCHANGE_EMAIL_BODY_MAX_CHARS

200000

Cap on get_email's body_text/body_html (1,000–5,000,000); longer bodies are truncated with truncated: true

EXCHANGE_EMAIL_MIME_MAX_SIZE_MB

25

Cap on raw MIME export size before base64 expansion (1–100)

EXCHANGE_SIGNATURE_TEXT

unset

Appended to outgoing text bodies and replies/forwards. No EWS signature API exists, so this is configuration, not the mailbox's Outlook signature

EXCHANGE_SIGNATURE_HTML

unset

Appended to outgoing HTML bodies. Same caveat as above; no cross-conversion between the two. Either can be skipped per call with include_signature: false

MCP_TRANSPORT

stdio

stdio, streamable-http, or the deprecated sse

MCP_HTTP_HOST

127.0.0.1

Bind host for the HTTP transports

MCP_HTTP_PORT

8080

Bind port for the HTTP transports

MCP_HTTP_AUTH_TOKEN

—

Required for the HTTP transports: bearer token clients must send (e.g. openssl rand -hex 32)

MCP_MAX_CONCURRENCY

4

Concurrent read-only tool calls (1–8); mutating calls always run exclusively. See Request queue

MCP_MAX_QUEUE_SIZE

20

Max calls admitted at once, running + waiting (1–1000); beyond that, calls get an immediate server_busy error

LOG_LEVEL

INFO

DEBUG, INFO, WARNING, or ERROR

LOG_FILE

unset (stderr)

Log file path; protect it with OS permissions if set

Behavior notes that aren't tied to a single variable:

  • list_events and find_free_slots accept a bounded limit (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_email has them), list_events returns events without bodies (get_event has them), and get_email returns RFC-822 headers only with include_headers: true.

  • Direct sends report submitted, not recipient delivery. send_draft looks 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 have downloadable: false and can't be saved by get_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; id is 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_unknown error: 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 (default 4) 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 (default 20) caps how many calls can be admitted at once, running or waiting. Once that many are already in, further calls get an immediate server_busy error 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 by EXCHANGE_TIMEOUT plus EXCHANGE_MAX_RETRY_WAIT_SECONDS: the account's retry policy is fail-fast, so every EWS call raises on its first transient error rather than exchangelib retrying it forever internally, and ExchangeClient retries 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-smoke

Default 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-smoke

Docker

docker build -t outlook-ews-mcp .
docker run --rm --env-file .env outlook-ews-mcp

CI/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 (v*) to PyPI via OIDC trusted publishing. Before the first release, configure a PyPI pending publisher for repository viartemev/outlook-ews-mcp, workflow ci.yml, and environment pypi — no long-lived PyPI token is stored in GitHub.

GitLab

Additionally builds and pushes a Docker image to the GitLab Container Registry on the default branch and on tags, using the built-in CI_REGISTRY / CI_REGISTRY_USER / CI_REGISTRY_PASSWORD / CI_REGISTRY_IMAGE variables.

Default image tagging behavior:

Trigger

Tags pushed

Default branch

:$CI_COMMIT_SHORT_SHA and :latest

Git tag

:$CI_COMMIT_TAG

Development

uv sync --extra dev
uv run ruff check .
uv run pytest -q

Project notes

  • The implementation is centered around a single ExchangeClient abstraction so auth, transport, retries, and error mapping stay centralized.

  • Errors are returned in a structured JSON form suitable for MCP isError=true handling.

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 tools
copy_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

    C2.6/5.0

    Scored across 31 tools

    Disambiguation4/5

    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.

    Naming Consistency5/5

    All tool names follow a consistent `verb_noun` pattern in snake_case (e.g., `create_event`, `list_emails`, `send_email`). No mixing of conventions.

    Tool Count3/5

    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.

    Completeness4/5

    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

    ActivityActive
    ResponsivenessLimited data

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      A
      quality
      C
      maintenance
      A 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.
      11
      8
      MIT
    • -
      license
      Not graded
      quality
      D
      maintenance
      Secure 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.
      -