Outlook MCP Server
by kennymatsudo
README.md
# Outlook MCP server
An MCP server that gives Claude, or any MCP client, access to your Outlook.com
or Microsoft 365 mailbox through Microsoft Graph. It runs on your machine as a
single Python file. You choose which kinds of changes it may make, mail is only
ever sent from a draft you approved, and every bulk action has to restate what
it is about to touch.
## Features
- Read: folders and subfolders, message lists, keyword or KQL search
(mailbox-wide or per folder), single messages, whole threads, attachments.
- Write: create, edit, reply to and forward drafts, with Markdown converted to
clean HTML, then send an approved draft.
- Clean up: move, archive, delete, categorize and junk-report in batches,
archive by criteria instead of by id, empty Junk or Deleted Items.
- Triage: rank senders by volume, find newsletters, old unread mail and large
messages, collect ids for a bulk action.
- Unsubscribe through the `List-Unsubscribe` header, only for senders that pass
DKIM and DMARC.
- Inbox rules: list, create, update and delete server-side rules, and suggest
rules from the senders that flood you.
## Requirements
- Python 3.10 or newer, and [uv](https://docs.astral.sh/uv/) or pip.
- An Outlook.com, Hotmail, Live, or Microsoft 365 account.
- A free Azure app registration (below). No paid subscription is needed.
## Installation
### Register an app in Azure
1. Open https://entra.microsoft.com, go to **App registrations** and choose
**New registration**.
2. Name it (for example `Outlook MCP`) and pick the account type:
personal accounts use **Personal Microsoft accounts only**; work or school
accounts use **Accounts in this organizational directory only**.
3. Leave the redirect URI empty and register. Copy the **Application (client) ID**.
4. Under **Authentication**, set **Allow public client flows** to **Yes**.
5. Under **API permissions**, add the Microsoft Graph delegated permissions
`User.Read`, `Mail.ReadWrite` and `Mail.Send`. Add
`MailboxSettings.ReadWrite` too if you want the inbox-rule tools. Work
accounts may need an admin to grant consent.
### Install the server
```bash
git clone https://github.com/kennymatsudo/outlook-mcp
cd outlook-mcp
uv sync
```
Or with pip:
```bash
python -m venv .venv && source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install .
```
### Sign in
Copy `.env.example` to `.env`, fill in `OUTLOOK_CLIENT_ID`, then:
```bash
uv run outlook-mcp login # pip: python outlook_mcp.py login
```
It prints a code and a URL. Open the URL, enter the code and approve. Refresh
tokens keep you signed in; run `login` again if a tool reports that the session
expired, and `logout` to remove the cached tokens.
## Configuration
All settings are environment variables. The server also reads a `.env` file
next to `outlook_mcp.py` (ignored by git); real environment variables win.
| Variable | Default | Meaning |
|---|---|---|
| `OUTLOOK_CLIENT_ID` | none, required | Application (client) ID from the Azure registration. |
| `OUTLOOK_TENANT` | `common` | `consumers` for personal accounts; `organizations` or your tenant ID for work accounts. |
| `OUTLOOK_PERMISSIONS` | `all` | Which groups of mail-changing tools exist: `all`, `read`, or a comma-separated list of groups (below). |
| `OUTLOOK_CACHE_DIR` | `~/.outlook-mcp` | Tokens and logs, created owner-only. |
| `OUTLOOK_DOWNLOAD_DIR` | `<cache dir>/downloads` | Where `save_attachment` writes files. |
Set them in whichever place fits:
```bash
# .env next to outlook_mcp.py
OUTLOOK_CLIENT_ID=00000000-0000-0000-0000-000000000000
OUTLOOK_TENANT=consumers
OUTLOOK_PERMISSIONS=draft,organize,attachments
```
```json
// the "env" block of your MCP client's server entry (see Usage)
"env": {
"OUTLOOK_CLIENT_ID": "00000000-0000-0000-0000-000000000000",
"OUTLOOK_TENANT": "consumers",
"OUTLOOK_PERMISSIONS": "draft,organize,attachments"
}
```
```bash
# Claude Code
claude mcp add outlook -e OUTLOOK_CLIENT_ID=... -e OUTLOOK_PERMISSIONS=read -- ...
```
Restart the client after changing a setting.
### Permission groups
Reading and triage tools are always available. Every tool that changes mail or
writes to disk belongs to one group. A tool in a disabled group is not offered
to the model at all, and the server tells the model which groups are off so it
says so instead of improvising.
| Group | Tools |
|---|---|
| `draft` | `create_draft`, `create_reply_draft`, `create_forward_draft`, `update_draft` |
| `send` | `send_draft` |
| `organize` | `move_messages`, `archive_messages`, `archive_matching`, `mark_read`, `mark_messages_read`, `categorize_messages`, `create_folder`, `report_junk`, `report_not_junk` |
| `delete` | `delete_messages` (to Deleted Items, recoverable) |
| `purge` | `permanently_delete_messages`, `empty_folder` (not recoverable) |
| `rules` | `create_rule`, `update_rule`, `delete_rule` |
| `unsubscribe` | `unsubscribe` |
| `attachments` | `save_attachment` (writes to the download directory) |
`OUTLOOK_PERMISSIONS=read` makes the server read-only.
`OUTLOOK_PERMISSIONS=draft,organize` lets the model write drafts and tidy
folders but never send, delete or change rules.
Within an enabled group, your client's own tool approval is the control that
holds. In the Claude app, set `send_draft`, `unsubscribe`, the delete, purge
and rule tools, `move_messages`, `report_junk` and `report_not_junk` to
*Ask before use*.
## Usage
Claude desktop app (Settings, Developer, Edit Config), with absolute paths:
```json
{
"mcpServers": {
"outlook": {
"command": "uv",
"args": ["run", "--directory", "/ABSOLUTE/PATH/outlook-mcp", "outlook-mcp"],
"env": {
"OUTLOOK_CLIENT_ID": "00000000-0000-0000-0000-000000000000",
"OUTLOOK_TENANT": "consumers"
}
}
}
}
```
With pip, the command is `/ABSOLUTE/PATH/outlook-mcp/.venv/bin/python` and
the args are `["/ABSOLUTE/PATH/outlook-mcp/outlook_mcp.py"]` (Windows:
`.venv\\Scripts\\python.exe`). Leave out `env` if you use a `.env` file.
Claude Code:
```bash
claude mcp add outlook -e OUTLOOK_CLIENT_ID=... -e OUTLOOK_TENANT=consumers \
-- uv run --directory /ABSOLUTE/PATH/outlook-mcp outlook-mcp
```
Things to ask:
- "What unread email do I have today?"
- "Draft a reply to the latest message from Alice saying I'll send the report Friday."
- "Who sends me the most mail? Unsubscribe me from the newsletters I never open."
- "Archive everything older than a year that I've already read."
- "Save the invoice attached to the message from Acme and tell me the total."
A full cleanup runs `mailbox_overview`, `sender_stats` and `find_bulk_mail` to
see where the volume is, `archive_matching` to clear the backlog,
`find_unsubscribable` then `unsubscribe` to stop newsletters, `suggest_rules`
then `create_rule` for senders that cannot be unsubscribed, and finally
`empty_folder` on Junk.
## Tools
| Area | Tools |
|---|---|
| Account and folders | `whoami`, `list_folders`, `mailbox_overview`, `create_folder` |
| Reading | `list_messages`, `search_messages`, `get_message`, `get_conversation`, `list_attachments`, `save_attachment` |
| Drafting and sending | `create_draft`, `create_reply_draft`, `create_forward_draft`, `update_draft`, `send_draft` |
| Organizing | `mark_read`, `mark_messages_read`, `categorize_messages`, `move_messages`, `archive_messages`, `archive_matching` |
| Deleting | `delete_messages`, `permanently_delete_messages`, `empty_folder` |
| Junk | `report_junk`, `report_not_junk` |
| Triage | `sender_stats`, `find_bulk_mail`, `messages_from_sender`, `find_old_unread`, `find_old_messages`, `find_large_messages`, `search_message_ids` |
| Unsubscribing | `find_unsubscribable`, `unsubscribe` |
| Inbox rules | `list_rules`, `suggest_rules`, `create_rule`, `update_rule`, `delete_rule` |
Each tool's description, shown to the model, says when to use it and what it
returns. A few behaviours worth knowing:
- Draft bodies are written as Markdown (paragraphs, lists, bold, italic,
links) and converted to HTML, so they look in Outlook the way they looked in
chat. `update_draft` refuses anything that is not a draft.
- A move gives each message a new id, returned as `new_message_ids`.
- `report_junk` trains Outlook's spam filter and blocks the sender, which
moving a message to Junk does not do.
- `save_attachment` never overwrites a file, and attached emails and cloud
links cannot be saved.
- Inbox rules run on Microsoft's servers, so they keep working when this
server is off, and they apply only to mail arriving after they are created.
They need the `MailboxSettings.ReadWrite` permission; if you add it later,
run `logout` and `login` again.
## Security
### Changes require a restated count
`move_messages`, `archive_messages`, `delete_messages`,
`permanently_delete_messages`, `report_junk`, `report_not_junk` and
`unsubscribe` fail unless `confirm_count` equals the number of items passed.
Called without it, they report what they would have touched so the model can
show you first. `archive_matching` previews the match count and ten samples,
needs an age or a sender so a whole folder can never be archived by accident,
and works in slices of 1000. `empty_folder` only accepts Junk or Deleted Items,
and its count must match the folder at the moment it runs. `delete_rule`
requires the rule's current name.
### Email is untrusted input
Email is written by strangers, and some of it will try to give the model
orders. The server marks and flags; it never blocks a tool call. The approval
steps above are what hold.
- Zero-width, bidi-override and Unicode tag characters are stripped from
everything returned to the model, so text invisible in Outlook never reaches
it. Tag characters can spell out hidden ASCII; that text is decoded and
scanned before it is stripped.
- Message bodies are fenced between `[BEGIN UNTRUSTED EMAIL CONTENT]` markers
(a forged marker inside an email is rewritten and flagged), and the server's
instructions tell the model never to follow text found in email.
- Subjects, previews, bodies and sender names are matched against a short list
of English attack phrasings: "ignore all previous instructions", role
overrides, text addressed to an AI, this server's own tool names, "forward
all emails to", a Markdown image with a query string, "without asking the
user", requests for the system prompt or an API key, and chat-template
markup. A match adds `injection_warning` to the result so the model can tell
you. Expect occasional false positives and expect a reworded attack to pass:
this is an alarm, not a filter, and it is deliberately not extended with
synonyms or other languages, because each addition adds more noise than cover.
- Every tool that changes mail appends to `audit_log.jsonl` in the cache
directory: tool, arguments and outcome. Bodies are logged as a length only.
### Unsubscribing is conservative
Both unsubscribe tools read only the `List-Unsubscribe` header, never links in
the body. `find_unsubscribable` groups senders by method:
| Method | Meaning | What `unsubscribe` does |
|---|---|---|
| `one_click` | RFC 8058 (https plus `List-Unsubscribe-Post`, DKIM-signed) | One fixed POST to a public https host, no redirects |
| `mailto` | Unsubscribe-by-email address | Outlook sends the email; that message goes to Deleted Items |
| `link_only` | Only a web page | Nothing; the link is returned for you to open |
| `none` | No unsubscribe header | Nothing; use `create_rule` |
| `skip` | In Junk, or failed DKIM/DMARC | Nothing; unsubscribing would confirm your address to a spammer |
Authentication comes from Microsoft's own `Authentication-Results` header, and
every such header must pass. Attempts are logged to `unsubscribe_log.jsonl`;
senders still mailing three days after a successful unsubscribe are flagged
`still_sending` on later runs and need a rule.
## Troubleshooting
| Message | Fix |
|---|---|
| `OUTLOOK_CLIENT_ID is not set` | Add it to `.env` or the client's `env` block. |
| `Not signed in to Outlook` | Run `login` with the same settings the server uses. |
| `AADSTS7000218` / public client error | Enable *Allow public client flows* in the app registration. |
| `AADSTS50020` / account not in tenant | Account type mismatch: check the registration's account type and `OUTLOOK_TENANT`. |
| `ErrorAccessDenied` from Graph | Add the missing API permission, then `logout` and `login`. |
| Rule tools say they need `MailboxSettings.ReadWrite` | Add that delegated permission, then `logout` and `login`. |
| `OUTLOOK_PERMISSIONS names unknown group(s)` | Use `all`, `read`, or names from the permission groups table. |
| A tool is missing from the model's list | Its group is off in `OUTLOOK_PERMISSIONS`; change it and restart the client. |
| `... call again with confirm_count=N` | Intended: the model must confirm the count with you first. |
| `ErrorItemNotFound` after a move | Ids change on move; use `new_message_ids` from the result. |
| Draft created "but its text could not be added" | The draft is in Drafts with only the quoted thread; delete it and retry. |
| A rule exists but mail is not moving | Check `has_error` and `enabled` in `list_rules`; rules only affect new mail. |
## Development
```bash
uv sync
uv run pytest
```
The tests cover everything that needs no mailbox: Markdown-to-HTML, injection
scanning, confirmation guards, sender authentication, the unsubscribe decision,
attachment file naming, draft editing and permission parsing.
## License
[MIT](LICENSE)
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo recent community issues