Skip to main content

outlook-mcp

MCP server for Microsoft Outlook personal accounts via Microsoft Graph API.

PyPI Python License: MIT MCP Registry

Personal Microsoft accounts only — @outlook.com, @hotmail.com, @live.com. Work/school accounts (Entra ID) are not supported: the server is built and tested on personal accounts. Fixes that make a tool behave on a work account, without changing what a personal account sees, are welcome.

Disclaimer: Independent open-source project. Not affiliated with, endorsed by, or supported by Microsoft Corporation. "Outlook" and "Microsoft Graph" are trademarks of Microsoft.


Who this is for

You'll like this if you're:

  • An agent builder wiring Outlook into your own infra (OpenClaw, Claude Code, Cursor, custom MCP host) and want a typed tool surface — not stdout you have to parse
  • Building on personal Microsoft accounts (Outlook.com / Hotmail / Live) and want full control: BYO Azure app, no enterprise consent flow, no shared client ID
  • Looking for real coverage — mail, calendar, contacts, to-do, drafts, folders, batch ops, threading — instead of a mail-only or calendar-only wrapper
  • Security-conscious: tokens in the OS keyring (Keychain on macOS, libsecret on Linux -- never cleartext unless you opt in), granular allow_categories, optional read_only mode, zero telemetry

This isn't for you if you need work/school M365 accounts (use Microsoft's official tooling — Entra ID auth and admin-consent flows are out of scope here), or if a basic mail-only client would suffice (this has 68 tools — way more than you need for "read my inbox").

How it differs from other Outlook tools you'll find

This is the only first-class MCP server in the personal-Outlook space — most alternatives are bash scripts or skill-shaped CLI wrappers the agent shells out to. That distinction matters: the agent gets typed tool schemas with structured args/returns, not stdout it has to parse. Other things you won't find elsewhere: /$batch-optimized triage (10-20× faster on bulk ops), recursive folder ops with name resolution, granular per-category permissions, multiple mailboxes (one server per account via OUTLOOK_MCP_CONFIG_DIR), and full attachment write paths including >3MB upload sessions for drafts.


What This Enables

Give your AI agent full Outlook access. Example prompts that just work:

  • "Summarize my unread email from the past 24 hours and flag anything time-sensitive."
  • "What's in my Focused Inbox right now? Anything in Other that looks like it belongs up top?"
  • "Any shipping updates in my inbox? Track what I'm waiting on and when it's supposed to arrive."
  • "Scan my email for upcoming subscription renewals — what's about to auto-charge in the next two weeks?"
  • "I've got a trip to Seattle next week — check my calendar for the itinerary and create a To Do task with a packing checklist."
  • "Draft a reply to the last message from my sister saying I'll call her this weekend."
  • "Move all newsletter and promotional email from this week to a 'Read Later' folder — batch 20 at a time."

The server exposes 68 discrete tools so the agent can compose its own workflow — read, triage, write, schedule, track tasks — without hardcoded macros.

Works With

  • OpenClaw — native MCP support, available via ClawHub
  • Claude Code — add to ~/.claude/settings.json under mcpServers
  • Cursor — MCP-compatible
  • Any MCP client — it's a standard stdio MCP server

Listed on the official MCP Registry as io.github.mpalermiti/outlook-mcp.


Features

68 tools across 13 categories:

  • Auth (1) -- auth status check (login is via CLI)
  • Mail Read (7) -- list inbox (with Focused Inbox and uncategorized filters), read message, bulk read by ID via $batch, search (KQL), list folders, delta-sync inbox changes, composed "since last call" digest across mail/events/contacts
  • Mail Write (3) -- send, reply/reply-all, forward
  • Mail Triage (9) -- move, delete (soft by default), flag, categorize, mark read/unread, reclassify (Focused Inbox), list/set/delete per-sender Focused Inbox overrides
  • Calendar Read (3) -- list events (with recurring expansion), get event details, delta-sync event changes
  • Calendar Write (4) -- create, update, delete, RSVP (accept/decline/tentative)
  • Contacts (7) -- list, search, get, create, update, delete, delta-sync changes
  • To Do (14) -- task lists, tasks (list/get/create/update/complete/delete), checklist items (add/update/delete), task attachments (list/download/upload/delete)
  • Drafts (5) -- list, create (new, or a reply, reply-all or forward in the message's thread), update, send, delete
  • Attachments (5) -- list, download, send-with-attachments, attach-to-draft, remove-draft-attachment
  • Folder Management (3) -- create, rename, delete mail folders
  • Threading and Batch (3) -- list thread, copy message, batch triage
  • User and Admin (4) -- whoami, list calendars, list categories, mail tips

Design principles:

  • BYOID -- Bring Your Own ID. You register your own Azure AD app. No shared client ID.
  • Zero telemetry -- no analytics, no local caching, no third-party calls.
  • Token storage -- OS keyring via azure-identity (macOS Keychain, Windows Credential Store, Linux Secret Service).
  • Input validation -- all inputs validated (email, Graph IDs, OData, KQL, datetimes) before any API call.
  • Read-only mode -- set read_only: true in config to block all write operations. Note this limits the tools, not the token -- see What read_only does and does not do.
  • Soft delete -- delete moves to Deleted Items by default. Hard delete requires explicit permanent: true.
  • Timezone-aware -- calendar operations respect your configured IANA timezone.
  • Relative dates -- every datetime parameter takes ISO 8601 or an offset: 7d is seven days ago, +7d is seven days from now, now is this moment. Units: m, h, d, w.
  • Bounded attachments -- attachment reads and writes are confined to attachments_dir, so a message that asks an agent to mail a file elsewhere on disk cannot be obeyed.
  • Bounded delta cursors -- a delta_token is caller-held state, so it is untrusted input. Every URL that would carry a Graph bearer token is parsed and required to be https on graph.microsoft.com, which is what stops a poisoned cursor from redirecting your mailbox token to someone else. Its path must also be the delta endpoint of the tool it was handed to, so a cursor cannot point a delta tool at some other part of the mailbox.
  • Workflow prompts -- morning_brief, triage_folder and catch_up ship as MCP prompts, so the common sequences do not have to be reconstructed call by call.

Agent-friendly shape (1.8.0)

Two pure-code upgrades that make the tools cheaper and more recoverable for AI agents:

  • Concise mode — pass concise=True to the six high-volume read tools (outlook_list_inbox, outlook_read_message, outlook_read_messages, outlook_search_mail, outlook_list_events, outlook_list_thread) to drop bulky fields: full message bodies, quoted prior-message text in threads, body previews and categories on inbox listings — typical payload reduction ~10×. On outlook_list_events the trade is different and smaller: the attendee list becomes a count, and organizer, response_status, type and show_as come off, so a concise scan cannot tell a recurring occurrence from a one-off. Default concise=False preserves the existing response shape — strict backward compat.

  • Structured Graph errors — every tool turns msgraph SDK exceptions into a tool error whose text carries Graph's message and a recovery hint: re-auth on 401, a link to the repo's ROADMAP dead-ends list on 403/ErrorAccessDenied, re-list on 404/ErrorItemNotFound, back-off on 429, retry on 503 — except on the tools that can email someone (mail, invitations, event updates, RSVPs), where a 5xx says the write may already have gone through and where to check, because those writes are never re-sent after one. OutlookMCPError subclasses and validation errors pass through unchanged.


Azure AD App Registration

You need to register a free Azure AD app to get a client ID.

Prerequisites (Personal Microsoft Accounts)

Microsoft has deprecated app registration for personal accounts without an Azure AD tenant. You need to create a free Azure account first:

  1. Go to azure.microsoft.com/free and sign up with your personal @outlook.com account. Requires a credit card for identity verification but won't charge you. This creates a proper Azure AD tenant.

Register the App

  1. Go to App Registrations and sign in with your @outlook.com account.

  2. Click "+ New registration" and fill in:

    • Name: anything except Microsoft-branded terms (e.g. mp-outlook-mcp — names like "Outlook MCP" will be rejected)
    • Supported account types: select "Personal Microsoft accounts only"
    • Redirect URI: leave blank
  3. Click Register. Copy the Application (client) ID from the overview page.

  4. Go to Authentication (Preview) → Settings tab → toggle "Allow public client flows" to Yes → Save.

  5. Go to API permissions → Add a permission → Microsoft Graph → Delegated permissions → add:

    • Mail.ReadWrite, Mail.Send
    • Calendars.ReadWrite
    • Contacts.ReadWrite, Tasks.ReadWrite
    • MailboxSettings.Read
    • User.Read, offline_access

No client secret is needed. The device code flow uses public client auth.


Quick Start

Install

Option A — from PyPI (recommended):

uv tool install outlook-graph-mcp
# or: pipx install outlook-graph-mcp
# or: pip install outlook-graph-mcp

Option B — from source:

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

Configure

Create ~/.outlook-mcp/config.json:

{
  "client_id": "YOUR_APPLICATION_CLIENT_ID",
  "tenant_id": "consumers",
  "timezone": "America/Los_Angeles",
  "read_only": true,
  "attachments_dir": "~/.outlook-mcp/attachments"
}

The only required field is client_id. Everything else has sensible defaults. Start with read_only: true — flip to false when you're comfortable.

Register with your MCP client

If installed from PyPI:

{
  "mcpServers": {
    "outlook": {
      "command": "outlook-mcp"
    }
  }
}

If installed from source:

{
  "mcpServers": {
    "outlook": {
      "command": "uv",
      "args": ["--directory", "/path/to/outlook-mcp", "run", "outlook-mcp"]
    }
  }
}

For OpenClaw, use the openclaw mcp CLI — it writes to mcp.servers in ~/.openclaw/openclaw.json for you:

# If installed from PyPI:
openclaw mcp set outlook '{"command":"outlook-mcp"}'

# If installed from source:
openclaw mcp set outlook '{"command":"uv","args":["--directory","/path/to/outlook-mcp","run","outlook-mcp"]}'

# Verify:
openclaw mcp list
openclaw mcp show outlook --json

Restart the OpenClaw gateway after registering. See the OpenClaw MCP docs for SSE/HTTP transport variants.

Authenticate

Run this once on the machine where the MCP server will run:

uv run outlook-mcp auth

You'll get a URL and a code. Open the URL in any browser, enter the code, and sign in with your Microsoft account. Tokens are cached in the OS keyring — the MCP server picks them up automatically.

The consent screen lists the full read-write set (Mail.ReadWrite, Mail.Send, Calendars.ReadWrite, Contacts.ReadWrite, Tasks.ReadWrite, MailboxSettings.Read, User.Read) — even when the config starts with read_only: true, because read_only gates the tools, not the token (see What read_only does and does not do), and the scopes a first consent leaves out can never be granted later without logging in again. (The one exception is read_only_consent: true, which asks for the read permissions only; it exists for a separate read-only app, described under the same heading.) Every request afterwards — silent refresh and each Graph call — uses the .default scope, which on an already-consented account means exactly "the set you granted". That ordering is deliberate: on personal accounts a first consent asking only for .default can land a session with no delegated permissions, which then can't be redeemed (AADSTS70000) without logging in again (#82).

Other CLI commands:

uv run outlook-mcp status   # Check auth status
uv run outlook-mcp logout   # Clear credentials
uv run outlook-mcp serve    # Start MCP server (default, used by OpenClaw/Claude)

Troubleshooting

me-token-to-replace is invalid on every call

You're on 1.22.0 installed after 2026-09-18. A fresh install of that version resolves microsoft-kiota-* 1.13 or later, which msgraph-core 1.5.1 doesn't handle, so every /me request reaches Graph as /users/me-token-to-replace. Fixed in 1.22.1, which caps kiota below 1.13 (#80). In 1.26.0 that cap is replaced by a msgraph-core>=1.5.2 floor, which carries the fix and works with kiota 1.13+:

uv tool upgrade outlook-graph-mcp
# or: pipx upgrade outlook-graph-mcp
# or: pip install --upgrade outlook-graph-mcp

Your config and sign-in are untouched; no re-auth needed.

SSL: CERTIFICATE_VERIFY_FAILED on Linux

If auth fails with [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate, your Python environment can't find the system CA bundle. This is common on minimal/container Linux images and with the isolated venv from uv tool install.

Point Python at your system CA bundle. Set both variables — auth (via azure-identity → requests) reads REQUESTS_CA_BUNDLE, while the delta/$batch paths (via httpx) read SSL_CERT_FILE:

export SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt      # httpx + Python ssl
export REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt  # azure-identity auth

The path varies by distro: Debian/Ubuntu use /etc/ssl/certs/ca-certificates.crt; RHEL/Fedora use /etc/pki/tls/certs/ca-bundle.crt. If the file is missing, install your distro's CA package (ca-certificates). Set these in the same environment your MCP client launches the server from so they apply at runtime, not just to the one-time auth command.

Token cache stored unencrypted (Linux)

A one-time startup warning about the token cache falling back to plaintext means libsecret/PyGObject isn't importable — see Privacy and Security for the fix.


Tool Reference

Dates. Every datetime parameter (after, before, start, end, due, deferred_send_datetime) accepts ISO 8601 — 2026-10-22 or 2026-10-22T14:30:00Z — or a relative offset: 7d is seven days ago, +7d is seven days from now, and now is this moment. Units are m, h, d, w. Bare means ago, matching the usual CLI convention, so a due date in the future needs the +. Zone-less input is interpreted in your configured timezone; responses are always UTC.

Auth

Tool Description
outlook_auth_status Check if authenticated and whether read-only mode is active.

Note: Authentication is handled via the CLI (outlook-mcp auth), not through MCP tools. See Authenticate above.

Mail Read

Tool Description
outlook_list_inbox List messages in a folder. folder accepts display names, well-known names, or Graph IDs. Filter by read status, sender, date range, Focused Inbox classification. Pagination via skip.
outlook_read_message Get full message by ID, or by its Message-ID header value (<abc@example.com>, matched exactly; the response's id is the Graph ID the other tools take). Format: text, html, or full (both). Pass include_deferred_send=True to also surface the draft's scheduled delivery time.
outlook_read_messages Bulk read up to 20 messages by ID via Graph $batch in one round-trip. Per-message shape matches outlook_read_message byte-for-byte for the same (format, concise, include_deferred_send). Partial-failure tolerant: 404s on some IDs surface in failures[] without failing the whole call. Use NOT N outlook_read_message calls.
outlook_search_mail Search mail using KQL query. Optionally scope to a folder by name or ID. A cursor continues the search in the folder it started in.
outlook_list_folders List mail folders with counts, parent_id, and child_count. Pass recursive=true to walk the full folder tree (subfolders included).
outlook_list_inbox_delta List only inbox changes since the last call. First call returns a full snapshot plus a delta_token; subsequent calls (token passed back) return only added/updated/deleted items. Deletes come back as {id, is_deleted: True}. Cursor is stateless — agent persists and replays.
outlook_changes_since One structured "since last call" digest composing mail/events/contacts deltas. Returns counts + urgent_flagged mail + top-5 by_sender + new/cancelled events. Each resource has an independent delta_token; stale-token recovery (HTTP 410) auto-resyncs that resource and surfaces _meta.resync. First-call snapshot is filtered to fallback_window_hours (default 24). Designed for recurring agent loops.

Mail Write

Tool Description
outlook_send_message Send email. Supports TO/CC/BCC, HTML or Markdown body, importance level.
outlook_reply Reply or reply-all to a message, the text above the quoted original. Plain text keeps its paragraphs, line breaks and - lists; a plain-text thread gets a plain-text reply.
outlook_forward Forward a message to one or more recipients with optional comment, which keeps its line breaks like a reply's text.

Bodies are plain text unless is_html=True (HTML, sent as written) or is_markdown=True. Markdown is converted to HTML for a safe subset: paragraphs, line breaks, -/*/+ and numbered lists, **bold**, *italic*, `code`, and [text](url) links to https: and mailto: addresses only. An image, ![alt](url), becomes a link to it, never an embedded picture. Everything else, raw HTML included, is shown as written. outlook_send_message, outlook_reply, outlook_forward (its comment), outlook_create_draft and outlook_update_draft take it; on a plain-text thread a Markdown reply is sent as written, as plain text.

Mail Triage

Tool Description
outlook_move_message Move a message to a folder by name or ID.
outlook_delete_message Delete a message. Soft delete (Deleted Items) by default. permanent: true for hard delete.
outlook_flag_message Set follow-up flag: flagged, complete, or notFlagged.
outlook_categorize_message Set categories on a message.
outlook_mark_read Mark a message as read or unread.
outlook_reclassify_message Move a message between Focused Inbox and Other (focused / other).
outlook_list_inbox_overrides List Focused Inbox per-sender override rules.
outlook_set_inbox_override Upsert a per-sender Focused Inbox override (focused / other). Case-insensitive sender matching; PATCH-if-exists, else POST.
outlook_delete_inbox_override Delete a Focused Inbox override by ID.

Calendar Read

Tool Description
outlook_list_events List events in a date range. Expands recurring events. Each event carries type — occurrence or exception for an instance of a series, singleInstance for a one-off — so a listing tells recurring work apart without a second call. A seriesMaster never appears here: calendarView returns expanded instances, so use outlook_get_event to see a master. Each event also carries show_as, the free/busy status Outlook labels "Show as". Configurable via days, after, before. calendar selects which calendar to read: omit (or "primary") for the default, otherwise a display name (case-insensitive; not-found and ambiguous errors name what exists) or an ID from outlook_list_calendars. A cursor continues the listing it came from, so later pages need neither calendar nor a second lookup. concise=True omits type and show_as.
outlook_get_event Get full event details: attendees, body, online meeting URL, recurrence, type (singleInstance / seriesMaster / occurrence / exception), show_as.
outlook_list_events_delta List only event changes inside a window since the last call. start and end (ISO 8601) required on the first call (Graph constraint — no whole-calendar sync). Each changed event carries every field the default outlook_list_events listing returns, type and show_as included, plus is_deleted (concise=True has its own narrower shape, which the delta tool does not mirror). Unlike the listing, this endpoint does return seriesMaster items, so a caller seeding from outlook_list_events should expect ids here that the seed never held. Deletes come back as {id, is_deleted: True} and nothing else. Cursor is stateless.

Calendar Write

Tool Description
outlook_create_event Create event with location and attendees. (is_online has no effect on personal accounts — Graph ignores isOnlineMeeting for consumer mailboxes.) Pass recurrence to create a series: a shorthand (daily, weekdays, weekly, monthly, yearly, anchored on start) or a full Graph recurrence object for anything else. range.startDate defaults to the event's start date. show_as sets the free/busy status Outlook labels "Show as" — free, tentative, busy, oof (out of office), workingElsewhere, or unknown; omit it and Graph applies its own default of busy. calendar creates it in another calendar, resolved as outlook_list_events resolves it: omit (or "primary") for the default, otherwise a display name or an ID from outlook_list_calendars. A calendar Graph lists as read-only (canEdit: false) is refused before anything is sent. The tools that take an event ID need no calendar: an ID reaches an event on any of your own calendars.
outlook_update_event Update event fields (subject, time, location, body, attendees, all-day, show_as). Only patches changed fields. Pass recurrence to turn a single event into a series, or remove_recurrence=True to turn a series back into a single event. attendees replaces the whole guest list and emails invitations/cancellations; is_all_day needs start+end in the same call, while show_as patches on its own. Patching a time keeps the zone the event is anchored in; timezone (with start+end) re-anchors it elsewhere, which is how a series created before zones existed gets repaired. A start, end or recurrence patch to a series with edited or deleted occurrences is refused, naming them — Graph would silently discard every one.
outlook_delete_event Delete a calendar event.
outlook_rsvp RSVP to an event: accept, decline, or tentative. Optionally include a message.

Contacts

Tool Description
outlook_list_contacts List contacts with cursor pagination. Summaries carry categories; outlook_search_contacts omits the key because Graph's $search does not return it.
outlook_search_contacts Search contacts by name or email. phone, instead of query, finds a number however it is written: with or without its country code, a leading 00 or trunk 0, spaces or dashes, from at least its last seven digits. Graph's contact $search finds a number only as it is stored, and phone fields can't be filtered, so a phone search reads the contact listing (pages of 1,000, up to 10,000 contacts) and compares digits. has_more is true when there were more matches than count, or contacts past the 10,000th.
outlook_get_contact Get full contact details by ID, including middle_name, job_title, birthday, home/business/other addresses, categories and personal notes.
outlook_create_contact Create a new contact. title is the name prefix ("Dr.", "Ms."), which Outlook shows in the display name; the job goes in job_title. phone_type (mobile, the default, business or home) says which kind of number phone is. birthday is a date (YYYY-MM-DD), stored at 11:59 UTC so that it is the same date in every time zone from UTC−11 to UTC+12; outlook_get_contact returns that timestamp. middle_name, personal_notes and categories are set as given.
outlook_update_contact Update contact fields, including middle_name, company, title, job_title and personal_notes. categories replaces the contact's whole list, as outlook_categorize_message does a message's, so [] removes every category; read the contact first to add one. phone_type works as on create; a business or home number replaces that list of numbers with the one given. clear_phones empties the types it names, so phone_type="business", clear_phones=["mobile"] moves a number saved as mobile to business in one update. home_address, business_address and other_address take the shape outlook_get_contact returns — any subset of street, city, state, postal_code, country_or_region — and replace that whole address, so pass back every part you want to keep. Omit one to leave it untouched. birthday sets or replaces the date, as on create. email replaces every address with the one given; add_emails and remove_emails change only the addresses they name, keeping the rest (a contact holds three at most). They are merged with what the contact holds now, and the update is pinned to that read with If-Match, so an edit made in between is refused with 412 rather than overwritten. Removing an address the contact doesn't have is refused.
outlook_delete_contact Delete a contact.
outlook_list_contacts_delta List only contact changes since the last call. Deletes come back as {id, is_deleted: True}. Cursor is stateless.

To Do

Tool Description
outlook_list_task_lists List To Do lists.
outlook_list_tasks List tasks with status filter and pagination.
outlook_get_task Get full task details: notes (body), checklist items (ordered unchecked-first), due, recurrence flag.
outlook_create_task Create task with due date, importance, recurrence.
outlook_update_task Update task fields.
outlook_complete_task Mark task as completed.
outlook_delete_task Delete a task.
outlook_add_checklist_item Add a checklist item (sub-step) to a task.
outlook_update_checklist_item Update a checklist item — mark done (is_checked) or rename (partial patch).
outlook_delete_checklist_item Delete a checklist item from a task.
outlook_list_task_attachments List attachments on a To Do task (id, name, size, content_type) with pagination.
outlook_download_task_attachment Download a task attachment's content to a local file (confined to attachments_dir), or return it with as_content=True.
outlook_upload_task_attachment Attach a local file (from attachments_dir) to a task via inline base64 POST (1 byte – 20 MiB).
outlook_delete_task_attachment Remove an attachment from a To Do task.

Drafts

Tool Description
outlook_list_drafts List draft messages with pagination.
outlook_create_draft Create a draft, with an HTML (is_html=True) or Markdown (is_markdown=True) body if wanted. With reply_to_message_id (reply_all=True for reply-all) or forward_message_id, the draft is created in that message's thread with the original quoted below, and nothing is sent; Graph fills a reply's recipients and subject, and a forward needs to. Supports scheduled delivery via deferred_send_datetime (server-side, Outlook-desktop-compatible "Delay Delivery").
outlook_update_draft Update draft fields. Accepts is_html=True for HTML bodies, is_markdown=True for Markdown ones, and deferred_send_datetime to set or clear the scheduled delivery time.
outlook_send_draft Send an existing draft.
outlook_delete_draft Delete a draft.

Attachments

Since 1.20.0, these tools only reach attachments_dir (default ~/.outlook-mcp/attachments). A bare filename resolves inside it; a path outside it is refused, including via a symlink. To email a file, move it there first — or widen attachments_dir, understanding that anything reachable from it can be sent. Before 1.20.0 these tools could read any file the server process could read, which meant an email asking an agent to attach one could be obeyed.

The same fence covers the To Do attachment tools (outlook_upload_task_attachment, outlook_download_task_attachment): uploads read from attachments_dir and downloads write into it, so neither can sweep arbitrary files off disk. (Downloads are not todo_write-gated — they are reads, like outlook_download_attachment; the fence, not the category, is what confines them.)

A client that can't see the server's disk, such as a remote or sandboxed one, can carry the bytes in the call instead, and then nothing is read from or written to disk. as_content=True on either download returns the attachment in the result, up to 10 MB: PNG, JPEG, GIF and WebP up to 5 MB as images, text up to 100,000 characters as text, and anything else, PDFs included, as an embedded resource holding the base64 bytes and their type. attachments on the two send tools takes inline files, [{name, content_base64, content_type?}], up to 3 MB each (Graph's simple-attachment size). The fence still governs every path argument. An inline file is bytes the agent already holds and could already put in a message body, so it widens nothing.

Tool Description
outlook_list_attachments List attachments on a message.
outlook_download_attachment Download an attachment and save decoded bytes into attachments_dir, or return it with as_content=True.
outlook_send_with_attachments Send a message with attachments read from attachments_dir (auto upload session for >3MB) or passed inline in attachments.
outlook_attach_to_draft Add attachments from attachments_dir (auto upload session for >3MB) or inline in attachments to an existing draft.
outlook_remove_draft_attachment Remove a single attachment from a draft.

Folder Management

Tool Description
outlook_create_folder Create mail folder (top-level or nested).
outlook_rename_folder Rename a mail folder.
outlook_delete_folder Delete a mail folder (refuses well-known folders).

Threading and Batch

Tool Description
outlook_list_thread Get all messages in a conversation thread.
outlook_copy_message Copy a message to another folder.
outlook_batch_triage Batch move/flag/categorize/mark_read (max 20 per call). Single Graph /$batch round-trip — 10-20× faster than per-message calls for large triage; a message Graph throttles is re-sent after its Retry-After rather than reported as failed. mark_read takes true/false (or read/unread).

User and Admin

Tool Description
outlook_whoami Get current user profile.
outlook_list_calendars List available calendars.
outlook_list_categories List category definitions with colors.
outlook_get_mail_tips Pre-send check (OOF, delivery restrictions).

Prompts

Three workflows ship as MCP prompts, so the common sequences do not have to be reconstructed call by call. Any MCP client that supports prompts will list them; in most clients they appear as slash commands or a prompt picker.

Prompt Arguments What it does
morning_brief folder (default inbox) Today's events, unread mail and tasks due, in the cheapest order — one scan each, concise=True, batched reads.
triage_folder folder (default inbox), count (default 50) One cheap scan of a folder, sorted into reply / archive / junk, applied with a single outlook_batch_triage call rather than one call per message.
catch_up since (default 24h) What changed in mail, calendar and contacts, via the delta path — roughly ten times cheaper than re-scanning on a schedule.

They cost nothing until invoked: prompts/list carries only a name and one line each, and the body is fetched on use.


Configuration

Config lives at ~/.outlook-mcp/config.json (created with 0600 permissions on macOS and Linux; see Config permissions below for Windows). It is read as UTF-8 on every platform, and a byte-order mark is accepted. Set the OUTLOOK_MCP_CONFIG_DIR environment variable to move that settings directory (config.json, auth record, and the attachments default move with it) — see Two accounts, two instances below.

Field Type Default Description
client_id string null Azure AD application (client) ID. Required for auth.
tenant_id string "consumers" Azure AD tenant. Use "consumers" for personal Microsoft accounts.
timezone string "UTC" IANA timezone (e.g. "America/New_York"). Interprets zone-less dates, and anchors every event you create — a recurring event is expanded in this zone, so on the default "UTC" a 09:00 weekly meeting shifts an hour when the clocks change. Set it to where you are.
read_only bool false When true, all write tools (send, reply, move, delete, create, update, RSVP) return an error. Gates the tools, not the Microsoft token -- see below.
read_only_consent bool false When true, outlook-mcp auth asks Microsoft for the read permissions only, instead of the read-write set. For a second, read-only app registration -- see What read_only does and does not do. Requires read_only: true; the config is refused without it.
attachments_dir string "~/.outlook-mcp/attachments" The only directory the attachment tools may read from or write to. Every path an agent supplies is resolved and must land inside it — a symlink out or a .. is refused. Widen it only if you understand that anything reachable can be emailed.
allow_categories list[string] [] Optional. Restrict write tools to specific categories (see below). Empty list = all writes allowed when read_only: false.
allow_unencrypted_token_cache bool false Permit the OAuth token cache to be written in cleartext when the platform has no encrypted store (Linux without libsecret). Off by default: authentication stops with an explanation rather than silently persisting a reusable Graph token in plaintext. macOS and Windows always encrypt and are unaffected.

Toolset selection (optional) — OUTLOOK_MCP_TOOLSETS

All 68 tool schemas load into the client's context every turn (the chars/4 proxy test_tool_surface_budget.py measures with; a different yardstick than the ~8.6k o200k figure in ROADMAP for the 62-tool surface). A client that only needs part of the surface can set the OUTLOOK_MCP_TOOLSETS environment variable to a comma-separated list of tool groups, and only those load. The account group (auth / identity) is always available.

# e.g. a recurring mail + calendar agent: 28 tools instead of 68 (about half the tool tokens per turn)
OUTLOOK_MCP_TOOLSETS="mail,calendar,digest,delta"

Groups: mail, drafts, attachments, calendar, contacts, todo, folders, digest, delta, admin. Unset (the default) loads everything — fully backward compatible. This only affects which tools are advertised; enabled tools behave identically.

Two accounts, two instances (optional) — OUTLOOK_MCP_CONFIG_DIR

One server process serves one mailbox. To work against two accounts, register two client entries and give each its own settings directory with OUTLOOK_MCP_CONFIG_DIR — pair it with OUTLOOK_MCP_TOOLSETS so each instance also only loads the tool groups it needs:

{
  "mcpServers": {
    "outlook-net": {
      "command": "outlook-mcp",
      "env": {
        "OUTLOOK_MCP_TOOLSETS": "mail,calendar,contacts",
        "OUTLOOK_MCP_CONFIG_DIR": "~/.outlook-mcp-net"
      }
    },
    "outlook-neko": {
      "command": "outlook-mcp",
      "env": {
        "OUTLOOK_MCP_TOOLSETS": "todo",
        "OUTLOOK_MCP_CONFIG_DIR": "~/.outlook-mcp-neko"
      }
    }
  }
}

Then run outlook-mcp auth once per instance, with the same env set, to write each auth record in its own directory. Each instance reads its own config.json (own client_id, timezone, permissions) from its own directory.

Only move the config directory — never HOME. The token cache is not in it: it stays in the OS keyring, and on macOS every azure-identity cache on the host shares one Keychain item, coordinated through a signal file — on this server, ~/.IdentityService/outlook-mcp.nocae (azure-identity appends .nocae to every non-CAE cache name; a CAE cache would be a different file over the same item). Both processes must keep consulting that same signal file so their cache writes lock and merge into the one shared entry — which is exactly what moving the config directory preserves and redirecting HOME (or the cache location) would break: two signal files that each believe they own the Keychain item overwrite each other's token. OUTLOOK_MCP_CONFIG_DIR deliberately moves only where config.json, the auth record, and attachments live; unset or empty keeps the default ~/.outlook-mcp.

What read_only does and does not do

read_only: true stops outlook-mcp's write tools from running. Ask it to send mail and it refuses.

It does not make your Microsoft credential read-only. The first sign-in consents the full read-write set, even with read_only: true in the config — the scopes a first consent leaves out can never be added without logging in again (every refresh afterwards uses .default -- "everything this account has already consented" -- so a session granted only read scopes would fail every write with 403 no matter what the config says). The stored token can send mail whether read_only is on or off, and a session consented before you turned read_only on keeps its write scopes.

Two consequences worth understanding:

  • read_only is a line in a text file. Anything able to edit ~/.outlook-mcp/config.json turns it off and has write access immediately -- no re-authentication, no new consent prompt.
  • The enforcement lives in this server's Python code. Any other process holding the cached token is unaffected by it.

So treat read_only as a guardrail against an agent doing something rash, not as a security boundary. If you want a credential that genuinely cannot write, have Microsoft enforce it rather than us:

  1. Register a second Azure app and give it the read permissions only: Mail.Read, Calendars.Read, Contacts.Read, Tasks.Read, MailboxSettings.Read, User.Read. It has to be an app this account has never granted write access to. Microsoft remembers consent per app, and a refresh returns everything that app was ever granted.

  2. Point client_id at it and set both keys:

    { "client_id": "<the read-only app>", "read_only": true, "read_only_consent": true }
    
  3. Run outlook-mcp auth. With read_only_consent the consent screen lists the read permissions only. Without it, sign-in would ask this app for the read-write set too.

read_only_consent is refused without read_only: true: a sign-in that asked only for read access cannot write, so a server expecting writes would fail on every one. And a sign-in belongs to the app it was made with. After client_id changes, the server will not use the old one — outlook-mcp status and outlook_auth_status say so until you run outlook-mcp auth again.

Granular Write Permissions (optional)

By default, read_only: false unlocks all write tools. For finer control, set allow_categories to restrict write access to specific categories. Read tools (list, search, get) are always allowed — allow_categories only narrows the write surface.

Available categories:

Category Tools Risk
mail_drafts create/update/delete draft, attach to/remove from a draft Safe — drafts only, no send
mail_triage move, delete (soft), flag, categorize, mark read, copy, batch, reclassify and set/delete Focused Inbox overrides Moderate — reversible except hard delete
mail_folders create/rename/delete folder Moderate
mail_send send, reply, forward, send_draft, send_with_attachments Dangerous — sends email on your behalf
calendar_write create/update/delete event, RSVP Moderate — your own calendar. The parts that email other people text the agent wrote need mail_send as well: inviting attendees, rewording (subject, body, location) any event that already has them, and adding a message to an RSVP. A bare RSVP, a time change and a cancellation still notify the people involved, but carry nothing the agent wrote
contacts_write create/update/delete contact Moderate
todo_write create/update/complete/delete task, checklist items; upload/delete task attachments Moderate — your own task list, but outlook_upload_task_attachment reads local files from attachments_dir and pushes their bytes to Graph, and task/checklist/attachment deletes are irreversible. Listing and downloading attachments are plain reads, gated like every other read (not at all) and fenced to attachments_dir

Example policies:

Draft-only assistant (agent can compose drafts, you review and send):

{ "read_only": false, "allow_categories": ["mail_drafts", "mail_triage", "todo_write"] }

(Note that todo_write includes the write-side task-attachment tools — file reads from attachments_dir, uploads to Graph, and irreversible deletes. Listing and downloading task attachments are reads and are not write-gated, like the mail attachment reads — see the table above.)

Calendar-only (agent can manage your schedule, nothing else):

{ "read_only": false, "allow_categories": ["calendar_write"] }

(It cannot invite anyone. An invitation is an email, so attendees need mail_send too — add it if the agent should set up meetings with other people, not just block out your own time.)

Full write access (agent can do everything):

{ "read_only": false }

Read-only (safest default, no writes):

{ "read_only": true }

When allow_categories is set, any tool in a non-allowed category returns a permission-denied error (PermissionDeniedError) naming the blocked category. When allow_categories is empty (or unset) and read_only is false, all write tools are permitted. read_only: true always takes precedence — if set, all writes are blocked regardless of allow_categories. Unknown category names are rejected at config load time with a validation error; only the seven names above are accepted.


Privacy and Security

  • Zero telemetry. No analytics, no tracking, no usage data collected.
  • Zero local caching. Every call goes directly to Microsoft Graph. No local email/calendar storage. (One carve-out: the To Do default-list id is resolved once per process and kept for the session — an id, not content; see outlook_list_tasks.)
  • Zero third-party calls. The server only talks to graph.microsoft.com and login.microsoftonline.com.
  • Token storage. OAuth tokens are persisted via azure-identity's TokenCachePersistenceOptions. On macOS the OS Keychain is used; on Windows, DPAPI; on Linux with PyGObject/libsecret available, gnome-keyring. On Linux without libsecret (e.g. the isolated venv created by uv tool install), tokens fall back to a 0600 plaintext file at ~/.IdentityService/ and the MCP logs a one-time warning at startup. For encrypted storage on Linux, install python3-gi gnome-keyring libsecret-1-0 and re-create the venv with --system-site-packages.
  • No logging of sensitive data. Message bodies, recipient addresses, and tokens are never logged.
  • Config permissions. On macOS and Linux the config directory is created 0700 and the config file 0600, and a loose mode on the file is repaired on load. On Windows those POSIX modes cannot be enforced — os.chmod there sets only the read-only attribute — so access is governed by the path's Windows ACL, including whatever it inherits from the directory it was created under, which this server neither applies nor verifies. Symlinked configs are rejected on every platform.
  • Input validation. All user inputs (email addresses, Graph IDs, OData filters, KQL queries, datetimes) are validated and sanitized before reaching the Graph API.

Development

# Install dev dependencies
uv sync --extra dev

# Run tests
uv run pytest

# Lint
uv run ruff check src/ tests/ scripts/

# Format (CI fails if `ruff format --check` would change a file)
uv run ruff format src/ tests/ scripts/

# Run server locally (stdio)
uv run outlook-mcp

Requirements: Python 3.10+


Roadmap

  • Advanced mail -- raw MIME export, internet message headers
  • Calendar -- cancel event (with attendee notification)
  • Work/school-only tools -- free/busy, meeting times, automatic replies, shared mailboxes: not planned for now (#133). If any are taken later, they come as an opt-in group that personal accounts never load

Committed direction, and what was investigated and ruled out (inbox rules, mailbox settings), is in ROADMAP.md.


License

MIT. See LICENSE.

Metadata

Release files for outlook-graph-mcp 1.27.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for outlook-graph-mcp 1.27.0
File Size Uploaded Provenance Actions
outlook_graph_mcp-1.27.0.tar.gz 189.2 kB Without provenance Details for outlook_graph_mcp-1.27.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for outlook-graph-mcp 1.27.0
File Interpreter ABI Platform Actions
outlook_graph_mcp-1.27.0-py3-none-any.whl Python 3 none any Details for outlook_graph_mcp-1.27.0-py3-none-any.whl

Total release size: 386.8 kB

Release files / outlook_graph_mcp-1.27.0.tar.gz

Download URL outlook_graph_mcp-1.27.0.tar.gz
Size 189.2 kB
Tags Source
SHA-256 checksum
How to use checksums
cc9cc63f9d5f6300687a609be22e9932ef1f0b453b0d34b55e95097228e08adf
BLAKE2b-256 checksum
How to use checksums
55287abd3fe0ad21f3d19821a400744122b3983627775865eb404ac3c095d9ef
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.13.5 {"installer":{"name":"uv","version":"0.13.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"26.04","id":"resolute","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / outlook_graph_mcp-1.27.0-py3-none-any.whl

Download URL outlook_graph_mcp-1.27.0-py3-none-any.whl
Size 197.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
2a373587e39d8557b602d5603c7a026aa2ddb0d306522d85929f82a0eb44cc51
BLAKE2b-256 checksum
How to use checksums
c1cdac312b5a2a403640edb6bdb6bbe26981f82adc9cbf6a7ee8240d271421f4
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.13.5 {"installer":{"name":"uv","version":"0.13.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"26.04","id":"resolute","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Security for outlook-graph-mcp 1.27.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

1.27.0 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page