Skip to main content
Glama
Outlook MCP Server

Outlook MCP Server

by kennymatsudo

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.

Related MCP server: Outlook MCP Server

Requirements

  • Python 3.10 or newer, and 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

git clone https://github.com/kennymatsudo/outlook-mcp
cd outlook-mcp
uv sync

Or with pip:

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:

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:

# .env next to outlook_mcp.py
OUTLOOK_CLIENT_ID=00000000-0000-0000-0000-000000000000
OUTLOOK_TENANT=consumers
OUTLOOK_PERMISSIONS=draft,organize,attachments
// 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"
}
# 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:

{
  "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:

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

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

Related MCP Connectors

Related MCP Servers