Skip to main content
Glama

Outlook MCP Server

Self-hosted MCP server that exposes a personal Outlook.com account (mail + calendar, via Microsoft Graph) to Claude.ai (or any MCP client) via a standard OAuth 2.0 flow - no pre-generated credentials, and no tokens persisted to disk (they live only in memory - see How it works).

Personal accounts only. This deliberately uses Microsoft's consumers-tenant endpoint, which structurally excludes work/school (Azure AD) accounts - only outlook.com / hotmail.com / live.com-style personal Microsoft accounts can ever sign in.

Sibling project to gmail-mcp-proxy, which does the same thing for Gmail - see Outlook vs Gmail below for where the two diverge.

  • Setup - Azure app registration + deployment walkthrough

  • Changelog - version history

Related MCP server: Outlook MCP Server

How it works

Claude.ai ──[OAuth]──► This server ──[OAuth]──► Microsoft
Claude.ai ──[MCP]────► This server ──[Graph API]──► Outlook mail / calendar

The server acts as an OAuth proxy: it presents itself as an OAuth 2.0 authorization server to Claude, and internally delegates authentication to Microsoft. After the user grants access, Microsoft Graph tokens are stored server-side (in memory) and injected per-request. A short-lived JWT is issued to Claude as the bearer token.

Multiple accounts - add the server twice in Claude with different alias URLs (/personal/mcp, /family/mcp). Each session is isolated; authenticate each with a different Microsoft account.

Tools

Tool

Description

get_profile

Outlook account profile, plus serverVersion (the running proxy version)

get_version

The running proxy version - compare with the Changelog to spot a stale cached tool list

search_emails

Search with Graph's $search syntax (from:, subject:, body:, received:, …)

list_messages

Messages inside one specific folder (id or well-known name like "inbox"), newest first; search_emails can't be scoped to a folder

read_message

Full message with decoded body and attachment metadata

read_conversation

All messages in a conversation (Outlook's equivalent of a Gmail thread), oldest first

get_attachment

Download an attachment's bytes (base64) by message_id/attachment_id - see ATTACHMENT_MAX_MB

send_email

Send, or reply within a conversation via reply_to_message_id; is_html=True for an HTML body

create_draft

Save a draft (is_html=True for an HTML body)

list_drafts

List drafts

send_draft

Send an existing draft

update_draft

Replace the content of an existing draft

delete_draft

Permanently delete a draft

list_folders

Top-level folders by default; pass parent_folder_id (a folder id, or a well-known name like "inbox") to drill into one, recursive=True to walk the whole subtree, name_contains to search by name, or minimal=True to trim the per-folder fields returned (cheaper for large recursive walks) - capped at 200 results

count_folders

Count folders in the mailbox (or one subtree via parent_folder_id) without returning the list itself - cheaper than list_folders(recursive=True) when the question is just "how many"

create_folder

Create a folder, optionally nested

update_folder

Rename a folder

delete_folder

Permanently delete a folder

move_folder

Re-parent a folder (and its contents/subfolders) under another folder

list_categories

The mailbox's master category list (color-coded tags)

update_categories

Add/remove categories on a message

move_message

Move a message to another folder

mark_as_junk

Move a message to Junk Email

trash_message

Move a message to Deleted Items

list_rules

Server-side inbox rules (see Server-side rules)

create_rule

Create a server-side rule by sender / recipient (delete / move / mark_read_only / categorize; does not mark read unless asked)

delete_rule

Delete a server-side rule

list_calendars

All calendars

list_events

Events in a time window (defaults to now-+30 days)

search_events

Search events by keyword

get_event

Single event by ID

Calendar is read-only in this version, and there are no Contacts tools - both are easy additions on Graph, deliberately deferred; see Outlook vs Gmail.

Prerequisites

  • A GitHub account with this repo forked (or cloned) into it

  • An Azure account (free) - used only to register an OAuth app, no billing needed

  • A place to run the server with HTTPS - self-hosting (Python 3.12+ or Docker) behind Tailscale or your own reverse proxy is the documented path; see Setup

Configuration

Variable

Description

MS_CLIENT_ID

Azure App registration Application (client) ID

MS_CLIENT_SECRET

Azure App registration client secret - expires within 24 months, see Notes

JWT_SECRET

Secret for signing session JWTs (any random string)

BASE_URL

Public base URL, no trailing slash, e.g. https://outlook-mcp-proxy.your-tailnet.ts.net

ALLOWED_REDIRECT_URIS

Optional. Comma-separated allowlist of OAuth redirect URIs /authorize will accept. Defaults to Claude.ai's callback (https://claude.ai/api/mcp/auth_callback) - only change this if you're connecting a non-Claude.ai MCP client.

LOG_LEVEL

Optional. Python logging level (INFO, WARNING, DEBUG, etc.). Defaults to INFO.

READ_ONLY_ALIASES

Optional. Comma-separated list of connector aliases (e.g. family) that should be restricted to read-only access - no send, draft, folder/category changes, rule changes, or move/trash. See below.

API_RETRY_ATTEMPTS

Optional. Total attempts (1-5) for an outbound Graph API call (read or write) before giving up on a retryable status (429/5xx). Defaults to 2. Honors a Retry-After response header from Graph when present, in either delay-seconds or HTTP-date form.

ATTACHMENT_MAX_MB

Optional. Max attachment size (1-25MB, decoded) get_attachment will fetch. Defaults to 3. Attachment bytes return as base64 text inside the MCP tool result - straight into the calling LLM's context, not just over the network.

PORT

Optional. Port the server listens on. Defaults to 8000.

Read-only accounts

To connect an account you want Claude to only ever read from - never send, delete, or modify - add its alias to READ_ONLY_ALIASES, e.g. READ_ONLY_ALIASES=family for a connector added at /family/mcp. That account's Microsoft OAuth grant will only ever request read scopes (Mail.Read, Calendars.Read, MailboxSettings.Read, plus the sign-in basics openid, offline_access and User.Read) - no write scope is ever issued for it, so even a bug in this server can't make it send or delete anything; Graph rejects it regardless. The server also refuses write tool calls itself with a clear error, as a second layer.

Enforcement is server-side and unconditional - which alias a request comes in through is derived from the URL path on every single request, both at /authorize (when deciding which Microsoft OAuth scopes to request) and on every /mcp call afterward, not something the client asserts. A restricted alias stays restricted even if its bearer token is ever presented to a different connector's endpoint.

Server-side rules

list_rules, create_rule and delete_rule manage Outlook inbox rules through Graph's mailFolders/inbox/messageRules. These run inside Exchange when mail arrives, so a rule that deletes or moves mail from a sender does not mark it read (marking read is a separate action you opt into) - unlike moving messages from a client.

  • They need the MailboxSettings.Read (list) and MailboxSettings.ReadWrite (create/delete) scopes. An account connected before these tools existed must be removed and re-added in Claude so the Microsoft consent screen grants them; a 403 mentioning MailboxSettings means that consent is missing.

  • create_rule works on the Inbox only and refuses a display_name that is already used. Conditions (at least one): sender_addresses (exact), sender_contains and recipient_contains (word match on the sender / To-Cc address, like Outlook's "sender address" and "recipient address" conditions). Actions: delete, move (needs a real folder id from list_folders), mark_read_only or categorize, and assign_categories can be added to any of them. Rules stop later rules by default; pass stop_processing=false for a rule (e.g. a categorize-only one) that should let the rest run.

  • list_rules also shows exceptions and hasError. There is no edit tool - delete and re-create.

Development

pip install -r requirements-dev.txt
pytest          # test suite
ruff check .    # lint
mypy            # type check (config in pyproject.toml)

Tests mock all Graph API calls (via respx) and cover the pure-logic helpers (PKCE, alias parsing, recipient parsing) plus tool behavior that's easy to get wrong - the consumers-tenant-only OAuth requirement, refresh-token rotation, read-only enforcement, and move_message's new-id semantics. No live Microsoft credentials needed to run them.

Outlook vs Gmail

Where this diverges from the Gmail sibling project:

Harder / more work:

  • Categories need read-modify-write (no atomic add/remove on Graph), vs. Gmail's atomic label add/remove.

  • Calendar listing needs two different endpoints (calendarView for time-range/recurrence-expansion vs. events/$search), vs. Gmail's single consistent surface.

  • move_message returns a new message id - anything holding the old id afterward will fail. Gmail message ids never change.

  • move_folder, by contrast, keeps the folder's original id - confirmed by live testing; only parentFolderId changes.

  • Folder hierarchy is a real tree (parentFolderId), vs. Gmail's flat /-named labels.

  • Two account-registration facts must be exactly right, with no forgiving fallback: the consumers-tenant-only endpoint, and "Personal Microsoft accounts only" in Azure Portal - get either wrong and work/school accounts could authenticate.

  • The Azure client secret expires within 24 months (Google's doesn't) - needs a calendar reminder, not a code fix.

  • Refresh tokens roll on every use and expire after ~90 days of account inactivity - a different failure mode than Google's, and the rotated token must always be re-stored.

Easier / genuine advantages:

  • read_message is simpler - body.content/body.contentType arrive as ready JSON, no MIME-tree walking or base64url decoding.

  • Attachment ids are stable across repeated reads of the same message - no Gmail-style partId/attachmentId mismatch bug to work around.

  • Reply is a native Graph action (/reply) - no manual In-Reply-To/References MIME header construction.

  • $search on messages returns full fields per hit already, with no separate enrichment round-trip needed (Gmail's search_emails needs one).

  • Graph explicitly documents and honors Retry-After - a more reliable throttling signal than Gmail's docs provide.

  • Calendar write, free/busy, findMeetingTimes, and a full Contacts API are all natively easy on Graph - deliberately deferred here, not technical gaps.

After a connector update

Claude keeps the tool list it loaded in each chat's history, so a chat can keep showing an older tool set after you upgrade the server. "Reconnect" does not refresh it. To get the new tools into an existing chat:

  1. Delete and re-add the connector in Claude.

  2. Restart the Claude app if it still shows the old tools.

  3. In the chat, run /compact - this reloads the tool definitions from the server.

  4. Only start a new chat if that still doesn't fix it.

To check, call get_version in the chat and compare it with the newest Changelog entry.

Notes

  • The running version is logged at startup (journalctl -u outlook-mcp-proxy | grep starting) and matches the newest Changelog entry.

  • If you expose the server with Tailscale Funnel, /authorize, /token and the OAuth metadata are reachable from the public internet by design (Claude's servers need them). They are bounded (a cap on pending sign-ins, PKCE and a redirect allowlist) and /mcp needs a valid bearer token, but treat the URL as public.

  • Sessions are stored in memory - a server restart requires re-authentication in Claude.ai

  • Runs as a single process - don't scale to multiple replicas or uvicorn --workers N. Session/state stores are per-process in-memory, so a request landing on a different process than the one that authenticated it would fail as if unauthenticated.

  • Microsoft access tokens are refreshed automatically using the stored refresh token, which Microsoft frequently rotates on every use - the server always stores whatever comes back.

  • Personal Microsoft account refresh tokens roll on a ~90-day inactivity window - an account genuinely unused for that long needs the same re-auth as after a restart.

  • The Azure app registration's client secret expires within 24 months. Rotate it in Azure Portal → your app → Certificates & secrets before then, update MS_CLIENT_SECRET, and restart the service.

  • The server issues 30-day JWTs; Claude re-authenticates when they expire.

Related MCP Connectors

Related MCP Servers

  • A
    license
    A
    quality
    A
    maintenance
    Lets Claude read email and read/write calendar events for personal Microsoft accounts (Hotmail/Outlook/Live) via Microsoft Graph API, filling the gap where the official connector only supports work/school accounts.
    6
    MIT
  • A
    license
    A
    quality
    D
    maintenance
    A local MCP server that connects Claude Desktop to a personal Hotmail/Outlook.com mailbox via Microsoft Graph API, enabling email management, rule handling, and composing messages.
    25
    MIT