Skip to main content

MCP server for Microsoft 365 Outlook (mail + calendar) with audit-preserving drafts and never-auto-send.

Project description

mcp-server-outlook

PyPI version Python versions Downloads Licence CI Coverage Contributions welcome

In one sentence: an MCP server that lets AI coding agents like Claude Code read your Microsoft 365 inbox and calendar — and draft emails and meetings on your behalf — without ever auto-sending and without breaking audit attribution.

What is this for?

You're a consultant, contractor, or operator working across multiple Microsoft 365 tenants. You'd love to let AI agents triage your inbox, summarise calendar conflicts, and draft replies — but every existing option is bad:

  • Account-bound claude.ai connectors force one tenant per connector — useless if you're in three tenants at once with the same Claude account.
  • IMAP/SMTP scrapers lose calendar context, lose attribution (mail appears as "anonymous client"), bypass modern auth.
  • Auto-sending agents are scary in regulated environments — one wrong tool call and you've sent an email "as you" that you never wrote.

mcp-server-outlook fixes all three:

  • Local process per tenant, multi-profile — run one MCP entry per customer / mailbox; tokens are namespaced.
  • Microsoft Graph for full attribution — drafts and reads carry the signed-in user's identity in the audit log.
  • Drafts only, send is human-only — there is no send_email tool, anywhere. The agent's reach ends at "draft saved".

Concretely, the agent gets these tools:

Tool What the agent does What ends up in Outlook
ol_email_search, ol_email_list_unread, ol_email_read finds and reads inbox nothing changes
ol_calendar_search, ol_calendar_list_events reads calendar nothing changes
ol_status shows pending drafts created by this profile nothing changes
(v0.2) ol_email_create_draft, ol_email_update_draft creates drafts in your Drafts folder a draft appears — you review and click Send
(v0.2) ol_calendar_create_event_draft creates a tentative event with responseRequested=False a draft event sits on your calendar — you send invites

Every action is attributed to the human who signed in once via Microsoft's standard Device Code login. No service-account "robot" identity. The default install does not request Mail.Send — the consent prompt does NOT include "this app can send mail as you", which is the line tenant admins (and your auditor) actually care about. Sending is opt-in via OUTLOOK_ALLOW_SEND=true (v0.3+) — see Sending: opt-in — and even when enabled, the agent never auto-sends; the human reviews each draft before any send tool call.

Installation

pip install mcp-server-outlook
# or, with uv (recommended):
uv tool install mcp-server-outlook
# or, on the fly without installing globally:
uvx mcp-server-outlook --help

Requires Python 3.11+. Works on Linux, macOS, Windows.

Quickstart

1. Sign in once (out of band)

uvx mcp-server-outlook login

Output looks like:

Sign in to mcp-server-outlook via the Device Code flow:
Open the URL in a browser and type the code.

     URL:   https://login.microsoft.com/device
     Code:  D2LKUY4AV

Waiting for sign-in...

Open the URL in any browser, type the code, sign in with your M365 account. Your refresh token is cached locally — see Token storage. The MCP server itself never blocks for human interaction afterwards.

Prefer to log in inside the MCP client without leaving your agent's chat? Use the Login from an MCP client flow below — same end result, no separate terminal needed.

2. Wire it into Claude Code

In your project's .mcp.json:

{
  "mcpServers": {
    "outlook": {
      "command": "uvx",
      "args": ["mcp-server-outlook"]
    }
  }
}

Restart Claude Code. The agent now has the ol_* read tools available — read-only by default.

3. Try it

You:    What unread mails do I have today that look actionable?
Agent:  [calls ol_email_list_unread → reads recent ones with ol_email_read]
        Three threads need a reply:
        - Anna (XMV) — asking about your availability for a Friday call
        - Markus (Customer Y) — needs sign-off on the SLA addendum
        - Calendar Bot — confirms tomorrow's standup at 10:00

You:    Read Anna's mail and check my Friday calendar.
Agent:  [calls ol_email_read + ol_calendar_list_events]
        Anna proposes Fri 14:00–15:00. You have a hard conflict 14:30–15:30
        with the Customer Y review. Suggest 13:00 or after 16:00.

In v0.2, draft tools become available so the agent can write the reply email or the counter-proposal event for you. You always click Send.

What it can do, in detail

Read tools (always available)

Tool Purpose
ol_email_search(query, folder?, from?, modified_after?, has_attachment?) Free-text search over the user's mailbox using Microsoft Graph $search. Returns hits with id, subject, from, received-at, snippet, web URL.
ol_email_list_unread(folder="Inbox", limit=50) Unread mails in the named folder, newest first.
ol_email_read(id, include_attachments=False) Full body (text + html) + headers + attachments-list for a single mail.
ol_calendar_search(query, calendar?, from_date?, to_date?) Events matching a free-text query.
ol_calendar_list_events(from_date, to_date, calendar="primary") Events in a date range with attendees and location.
ol_status() Pending drafts created by this MCP profile (empty in v0.1; populated once draft tools land in v0.2).

Write tools — deferred to v0.2 (opt-in via OUTLOOK_ALLOW_DRAFTS=true)

Tool Purpose
ol_email_create_draft(to, subject, body, in_reply_to?, cc?, bcc?, attachments?) Creates a draft in the user's Drafts folder. Returns draft_id + web_url.
ol_email_update_draft(draft_id, …) Updates a draft created by this profile.
ol_email_list_drafts(profile_only=True) Lists drafts created by this profile.
ol_email_discard_draft(draft_id) Deletes a draft created by this profile.
ol_calendar_create_event_draft(subject, start, end, attendees?, body?, location?) Creates a tentative event with responseRequested=False so no invites auto-send.
ol_calendar_discard_event_draft(event_id) Removes an event draft created by this profile.

Explicitly NOT exposed (by design)

  • send_email / send_draft — human-only action, perform in Outlook UI.
  • send_calendar_invitation — same.
  • delete_email / archive_email — read-only on inbox; user manages their own mailbox state.
  • Bulk operations on mails the user did not author or did not create as drafts via this MCP — defensive against fat-finger mass changes.

Authentication

  • OAuth 2.0 Device Code flow against Microsoft Identity (default). You sign in once; the refresh token is cached locally and silently renewed (~60–90 days until full re-login).
  • Bring-your-own-app or use ours. XMV publishes a multi-tenant Entra app registration that's baked in as the default — same pattern as Azure CLI / GitHub CLI. Tenants with strict app-allowlisting can override via OUTLOOK_CLIENT_ID and OUTLOOK_TENANT_ID env vars.
  • Token storage is auto-detected at first use: OS keyring (macOS Keychain / Windows Credential Locker / Linux Secret Service) when available, mode-0600 plain JSON file as fallback (same convention as gh auth, aws configure). Optional encryption with OUTLOOK_TOKEN_PASSPHRASE for paranoid setups or CI.
  • Multi-customer / multi-tenant: separate OUTLOOK_PROFILE per tenant, each with its own token cache.

Required Microsoft Graph scopes (delegated)

  • Mail.Read — read inbox + folders
  • Mail.ReadWrite — create / update / discard drafts (v0.2)
  • Mail.Send — send a draft (v0.3+). Only requested when OUTLOOK_ALLOW_SEND=true is set. The default install never requests this scope; the consent screen stays drafts-only.
  • Calendars.Read — read events
  • Calendars.ReadWrite — create / update / discard event drafts (v0.2)
  • User.Read — basic profile (signed-in user identification)
  • offline_access — refresh tokens

The default install never requests Mail.Send. Sending is opt-in via OUTLOOK_ALLOW_SEND=true (v0.3+); see the section below. Even with the opt-in active, the agent never auto-sends — every send requires an explicit ol_email_send_draft(draft_id) tool call referencing a draft the human has already reviewed in Outlook.

Sending: opt-in via OUTLOOK_ALLOW_SEND

For agent-driven workflows that close the loop ("draft, review, send" all in one MCP session), v0.3 adds an opt-in send tool. To enable it:

{
  "mcpServers": {
    "outlook": {
      "command": "uvx",
      "args": ["mcp-server-outlook"],
      "env": {
        "OUTLOOK_ALLOW_DRAFTS": "true",
        "OUTLOOK_ALLOW_SEND": "true"
      }
    }
  }
}

What this changes:

  1. The next mcp-server-outlook login request additionally asks for Mail.Send consent. Microsoft's consent screen will explicitly include "send mail as you" — you have to click Approve.
  2. The MCP server registers an additional tool: ol_email_send_draft(draft_id). It refuses to send any draft that's not in the per-profile registry (i.e. only sends drafts the agent itself created).
  3. Still no autonomous send. The agent must explicitly call ol_email_send_draft(draft_id) with a specific draft id. Human-in-the-loop on every send: the user reviews the draft body and recipients in Outlook between ol_email_create_draft and ol_email_send_draft.

If you're unsure whether you want this, leave the flag out. The default drafts-only mode is sufficient for the most common operator workflows.

Service-principal mode (unattended automation)

For CI / scheduled jobs where no human is in the loop, run with OUTLOOK_AUTH_MODE=service-principal (or just set OUTLOOK_CLIENT_SECRET — auto-detected). Required env vars: OUTLOOK_CLIENT_ID, OUTLOOK_CLIENT_SECRET, OUTLOOK_TENANT_ID. The app registration must have Application Microsoft Graph permissions with admin consent recorded.

Tradeoff: every action is attributed to the application principal, NOT a real user. The compliance-friendly default stays delegated user auth — only switch when no human is in the loop.

Login from an MCP client

Two MCP tools — ol_login_begin and ol_login_status — let an agent drive the OAuth Device Code flow without leaving the chat. This is the recommended path for any client where shelling out to a terminal is awkward (web UIs, mobile, locked-down sandboxes). The CLI mcp-server-outlook login path stays — same token cache, fully interoperable. Use whichever fits the moment.

The two tools:

Tool What it does
ol_login_begin(force=False) Initiates the Device Code flow, returns user_code + verification_url, polls Microsoft Identity in the background, blocks until terminal (success / expired / failed). Streams progress notifications during the wait when the client supports them. Idempotent: a non-expired pending session for the profile is joined, not duplicated. force=True cancels any in-flight session and starts fresh.
ol_login_status() Returns one of three states: signed_in (a usable token exists for this profile, regardless of how it got there — CLI login hours ago, tool login just now), pending (a Device Code flow is in flight; response carries user_code + verification_url so the agent can re-display the prompt), or none (no token, no flow — call ol_login_begin). Read-only.

Crucial property of ol_login_status: it actively probes the token store. A user who logged in via the CLI three days ago shows as signed_in, NOT none. The two paths don't fight; they cooperate via the shared token cache.

Typical flow as the agent sees it:

You:    Help me triage my unread mail.
Agent:  [ol_login_status() → "none"]
        I need to sign in to your Microsoft 365 account first.
        [ol_login_begin() → returns pending session]
        ABCD-EFGH

        https://login.microsoftonline.com/common/oauth2/deviceauth

        Tap-and-hold the code to copy, then click the link.
        I'm waiting…
You:    [signs in in browser]
Agent:  [ol_login_begin() returns success]
        Signed in as anna@xmv.de.
        [ol_email_list_unread() → ...]
        You have 3 unread mails. The first is from …

Pending sessions are in-process state. If the MCP server restarts before the user completes the Device Code prompt (Claude Code session ends, container redeployed, …), the in-flight session is lost. The agent calls ol_login_begin again — the Microsoft side cleans up the abandoned device code automatically. Persisting pending sessions to disk is non-trivial (the asyncio polling task can't be serialised; resuming from a fresh process would need to start a new poll against the original device code) and is deferred to a later release. In practice this rarely matters — Device Code flows take 30–60 seconds when the user is active.

No ol_logout MCP tool. Logout stays CLI-only (mcp-server-outlook logout --profile <name>). An agent proactively logging users out is a footgun — the kind of "agent decided to be helpful" surprise that causes incident reports. Human-initiated logout is the expected path.

File-lock caveat (rare). If you run the CLI mcp-server-outlook login --profile foo concurrently with ol_login_begin(profile="foo") from a running MCP server, the two writes to the token cache file are not protected by a file lock at the library level. The result on collision is "last writer wins" — typically harmless because both paths produce a valid token, just one of them ends up on disk. If you care about strict correctness here, run only one of the two paths at a time.

Safety model

Four layers of "don't accidentally do something irreversible":

  1. Your MCP client (Claude Code) prompts before each tool call by default. Read tools are flagged read-only; draft tools are flagged "creates draft (no send)"; the v0.3+ send tool is flagged destructive — you see the difference at the prompt.
  2. Drafts opt-in via env (v0.2). Without OUTLOOK_ALLOW_DRAFTS=true, the draft-creation tools aren't even registered. The agent literally can't draft.
  3. Send opt-in via a separate env (v0.3). Without OUTLOOK_ALLOW_SEND=true, the send tool is not registered AND Mail.Send is not in the OAuth scope request. The default consent screen stays drafts-only.
  4. Never autonomous send. Even with both opt-ins active, the agent must explicitly call ol_email_send_draft(draft_id) with a specific draft id. There is no path where an agent's tool call results in a sent email without the human first being able to read the draft body and recipients in Outlook.

The threat model is "your local OS account is trusted" — same as ~/.ssh/id_rsa, gh tokens, aws config. The tool isn't designed to defend against host compromise; it's designed to keep audit trails honest under normal use.

Roadmap

Version Status Theme Highlights
v0.1 🛠️ in progress Read-only inbox + calendar The six ol_* read tools, three-layer test harness, Trusted-Publisher PyPI release pipeline, branch-protected main.
v0.2 📋 planned Drafts ol_email_create_draft / ol_email_update_draft / ol_email_list_drafts / ol_email_discard_draft, ol_calendar_create_event_draft / ol_calendar_discard_event_draft, attachment access, per-profile draft registry.
v1.0 🎯 stability lock-in "API stable, production-tested" After v0.x has been used in real customer environments for ~3–6 months without breaking changes.

The full ticket-by-ticket plan lives at the issues page.

Multi-profile pattern

For consultancy workflows with multiple Microsoft 365 tenants, give each its own profile so the token caches don't collide:

{
  "mcpServers": {
    "outlook-acme": {
      "command": "uvx",
      "args": ["mcp-server-outlook"],
      "env": { "OUTLOOK_PROFILE": "acme" }
    },
    "outlook-globex": {
      "command": "uvx",
      "args": ["mcp-server-outlook"],
      "env": { "OUTLOOK_PROFILE": "globex" }
    }
  }
}

Sign each one in separately:

uvx mcp-server-outlook login --profile acme
uvx mcp-server-outlook login --profile globex

Tools appear in Claude as mcp__outlook-acme__ol_email_search etc. Cross-tenant accidents don't happen because the tokens are namespaced.

BYO Entra app registration

Tenants with strict app-allowlisting can override the bundled multi-tenant default:

{
  "mcpServers": {
    "outlook": {
      "command": "uvx",
      "args": ["mcp-server-outlook"],
      "env": {
        "OUTLOOK_TENANT_ID": "<your-tenant-guid>",
        "OUTLOOK_CLIENT_ID": "<your-app-registration-guid>"
      }
    }
  }
}

The app registration must be: multi-tenant or single-tenant, public client (no secret), Device Code flow allowed, with delegated permissions Mail.Read, Mail.ReadWrite, Calendars.Read, Calendars.ReadWrite, User.Read, offline_access. Optionally add Mail.Send if you intend to enable OUTLOOK_ALLOW_SEND=true for an opt-in send tool — the runtime auth flow only requests this scope when that env var is truthy.

Token storage

Three backends, auto-detected at first use:

Tier Backend When Setup
1 OS keyring macOS Keychain / Windows Credential Locker / Linux with Secret Service none
2 Plain file ~/.cache/outlook-mcp/<profile>/token.json mode 0600 Headless Linux default none
3 Encrypted file (Fernet, Scrypt KDF) When OUTLOOK_TOKEN_PASSPHRASE is set env var

Force a specific backend with OUTLOOK_TOKEN_STORE=keyring|file|encrypted-file.

Troubleshooting

"No usable credentials"

The cached token expired (refresh tokens last ~60–90 days) or never existed. Run:

uvx mcp-server-outlook login --profile <name>

Linux: keyring fails / "Secret Service unavailable"

The plain-file backend kicks in automatically — no action needed. If you'd rather have encryption at rest:

export OUTLOOK_TOKEN_PASSPHRASE='<some-strong-passphrase>'
uvx mcp-server-outlook login --profile <name>

Development

git clone https://github.com/XMV-Solutions-GmbH/outlook-mcp.git
cd outlook-mcp
uv sync --extra dev

# Unit + integration (no real Microsoft Graph), with coverage reporting
./tests/run_tests.sh

# Harness (real Microsoft 365 sandbox; requires harness-profile login)
./tests/run_tests.sh harness
Document What's in it
docs/app-concept.md Vision, MVP scope, MCP tool surface, auth, safety model
docs/testconcept.md Three-layer test strategy (unit / integration / harness)
ENGINEERING_PRINCIPLES.md Project-agnostic engineering baseline
CLAUDE.md Project-specific overrides

Sister project

  • mcp-server-sharepoint — same authorship pattern, audit-preserving SharePoint document edits with checkout/checkin.

Contributing

Contributions are welcome. Please read CONTRIBUTING.md and the Code of Conduct first.

Bug reports and feature requests go to GitHub Issues.

Licence

Dual-licensed under either of:

at your option.

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this project by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.

Contact

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

mcp_server_outlook-0.3.1.tar.gz (225.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

mcp_server_outlook-0.3.1-py3-none-any.whl (70.7 kB view details)

Uploaded Python 3

File details

Details for the file mcp_server_outlook-0.3.1.tar.gz.

File metadata

  • Download URL: mcp_server_outlook-0.3.1.tar.gz
  • Upload date:
  • Size: 225.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.12 {"installer":{"name":"uv","version":"0.11.12","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for mcp_server_outlook-0.3.1.tar.gz
Algorithm Hash digest
SHA256 ab516c637d0fa382ca6b74814bfdb6b25762b0b41141edc256ef580d308fa8af
MD5 b92fae606abe5e9e2a2ec46819787e3b
BLAKE2b-256 7e7ae39817826065059759cb1cca7b904b73c723fb41c49a990e451f5c20eb9d

See more details on using hashes here.

File details

Details for the file mcp_server_outlook-0.3.1-py3-none-any.whl.

File metadata

  • Download URL: mcp_server_outlook-0.3.1-py3-none-any.whl
  • Upload date:
  • Size: 70.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.12 {"installer":{"name":"uv","version":"0.11.12","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for mcp_server_outlook-0.3.1-py3-none-any.whl
Algorithm Hash digest
SHA256 f75d31879ca32333fa88e56f15f5afb891c5f5c9d3e1ef823e613b9f55abf42e
MD5 400f44ca08277cf91d3dac73c21977ee
BLAKE2b-256 99ced4437272ab24a586fdbfe67efc8ccbd4ff2533e27459eecfc3da072b0717

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page