MCP server for Microsoft 365 Outlook (mail + calendar) with audit-preserving drafts and never-auto-send.
Project description
mcp-server-outlook
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_emailtool, 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. No Mail.Send permission ever requested — the consent prompt explicitly does NOT include "this app can send mail as you", which is the line tenant admins (and your auditor) actually care about.
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.
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_IDandOUTLOOK_TENANT_IDenv 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 withOUTLOOK_TOKEN_PASSPHRASEfor paranoid setups or CI. - Multi-customer / multi-tenant: separate
OUTLOOK_PROFILEper tenant, each with its own token cache.
Required Microsoft Graph scopes (delegated, v0.1)
Mail.Read— read inbox + foldersCalendars.Read— read eventsUser.Read— basic profile (signed-in user identification)offline_access— refresh tokens
In v0.2 (drafts), Mail.ReadWrite and Calendars.ReadWrite are added. Mail.Send is never requested, ever — sends remain a human action in Outlook.
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.
Safety model
Three layers of "don't accidentally do something irreversible":
- 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)" — you see the difference at the prompt.
- 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. - No send tools — anywhere. Sending is exclusively a human action in Outlook. This is structural, not config-flag-able. Even with
OUTLOOK_ALLOW_DRAFTS=true, sends remain manual.
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, Calendars.Read, User.Read, offline_access (plus Mail.ReadWrite, Calendars.ReadWrite once v0.2 ships drafts). Do not grant Mail.Send — this MCP never sends.
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:
- Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
- MIT licence (LICENSE-MIT or http://opensource.org/licenses/MIT)
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
- Organisation: XMV Solutions GmbH
- Email: oss@xmv.de
- Website: https://xmv.de/en/oss/
- GitHub: @XMV-Solutions-GmbH
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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file mcp_server_outlook-0.1.0.tar.gz.
File metadata
- Download URL: mcp_server_outlook-0.1.0.tar.gz
- Upload date:
- Size: 178.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.11 {"installer":{"name":"uv","version":"0.11.11","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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0d6370a013da9aab7bdefc709faaf30bc4b9825757d9ac774aa5fccb0ce6b176
|
|
| MD5 |
4b62ac17dead5e4c1f6b281fa4271143
|
|
| BLAKE2b-256 |
3d9532049d468445bdd692fc6463914ce70c966d0d05b352357081bb66d1a57c
|
File details
Details for the file mcp_server_outlook-0.1.0-py3-none-any.whl.
File metadata
- Download URL: mcp_server_outlook-0.1.0-py3-none-any.whl
- Upload date:
- Size: 46.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.11 {"installer":{"name":"uv","version":"0.11.11","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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d71dab17e6dda3d889dc19db1e39c09d0a17fd491556ba667c412843a570789f
|
|
| MD5 |
08507eed7fa9b69e3f360bf27c529ac7
|
|
| BLAKE2b-256 |
ae34fc35b8f6648cf44fb239245431e40a3e1a62cffc1959551906ff489d8225
|