outlook-mcp/README.md

18 KiB

Outlook MCP Server

A Model Context Protocol server that exposes Microsoft Outlook email, calendar, folders, and inbox rules through the Microsoft Graph API.

Built for use with AI agents (Claude, Hermes, etc.) that support the MCP standard — gives your assistant the ability to read, search, and send email, manage calendar events, organize folders, and automate inbox rules.

Features

  • Email: List, search, read, send, and reconstruct full conversation threads with quoted-reply stripping and signature deduplication
  • Shared mailboxes: Optional mailbox parameter on email/folder tools; list-mailboxes probes seeded candidates (Graph cannot enumerate all rights)
  • Calendar: List, create, decline, cancel, and delete events
  • Folders: List (flat or hierarchical), create, and move emails between folders
  • Inbox Rules: List, create, and reorder execution priority of inbox rules
  • Authentication: MSAL device code flow with persistent token cache — no browser redirect needed
  • Test Mode: Mock data layer for development without a real Microsoft account

Quick Start

Prerequisites

  • Node.js ≥ 14
  • A Microsoft Azure App Registration (see Azure Setup below)

Install

git clone <repo-url> outlook-mcp
cd outlook-mcp
npm install

Configure

cp .env.example .env
# Edit .env and set MS_CLIENT_ID

Run

npm start

The server communicates over stdio (standard MCP transport). Your MCP client (Claude Desktop, Hermes, etc.) launches it as a subprocess.

Authenticate

When you first use the server, call the authenticate tool. It will return a URL and a short code:

1. Open: https://microsoft.com/devicelogin
2. Enter code: ABC123XYZ

You have 15 minutes to complete sign-in.

Visit the URL on any device, enter the code, and sign in with your Microsoft account. Tokens are cached to disk (~/.outlook-mcp-tokens.json) and silently refreshed by MSAL — you won't need to re-authenticate until the refresh token expires.

To check auth status without side effects, use check-auth-status (not authenticate).

Azure App Registration

This server uses MSAL device code flow with a public client app registration.

  1. Go to Azure Portal → App RegistrationsNew registration
  2. Name it (e.g. "Outlook MCP")
  3. Supported account types: Accounts in any organizational directory and personal Microsoft accounts (multi-tenant)
  4. Redirect URI: leave blank (device code flow doesn't need one)
  5. After creation, go to Authentication → scroll to Advanced settings → enable Allow public client flows
  6. Go to API permissions → add Delegated Microsoft Graph permissions:
    • Mail.Read
    • Mail.ReadWrite
    • Mail.Send
    • Mail.Read.Shared (shared/delegated mailboxes — work/school only)
    • Mail.ReadWrite.Shared
    • Mail.Send.Shared
    • User.Read
    • Calendars.Read
    • Calendars.ReadWrite
    • MailboxSettings.ReadWrite
    • offline_access
  7. Grant admin consent for the tenant (required for *.Shared in most orgs)
  8. Copy the Application (client) ID — that's your MS_CLIENT_ID

No client secret is needed for public client apps.

After adding shared scopes to an existing deployment, users must re-run device-code authenticate so the access token's scp claim includes the new permissions. check-auth-status reports missing shared scopes.

Configuration

All configuration is via environment variables:

Variable Required Default Description
MS_CLIENT_ID Yes Azure app registration client ID
MS_CLIENT_SECRET No Only for confidential client apps; leave blank for public client
USE_TEST_MODE No false Use mock data instead of real API calls
DEBUG_MODE No false Verbose logging (MSAL, API calls, working directory)
OUTLOOK_TOKEN_STORE_PATH No ~/.outlook-mcp-tokens.json Token cache file path (override for multi-instance setups)
MS_TIMEZONE No America/New_York Default timezone for calendar/event display and date filters (IANA or Windows name)
OUTLOOK_ENABLE_SHARED_MAILBOXES No true When false, omit shared scopes and hide list-mailboxes
OUTLOOK_SHARED_MAILBOXES No empty Comma-separated seed list of shared mailbox UPNs/SMTPs to probe
OUTLOOK_MAILBOX_CACHE_PATH No ${tokenStore}.mailboxes.json Probe result cache path
OUTLOOK_MAILBOX_PROBE_CONCURRENCY No 4 Parallel probes in list-mailboxes
OUTLOOK_MAX_RETRIES No 3 Graph 429/503 retry count
OUTLOOK_BASE_RETRY_DELAY_MS No 1000 Initial backoff delay
OUTLOOK_MAX_RETRY_DELAY_MS No 30000 Backoff ceiling

Shared mailboxes

Microsoft Graph does not expose an API that lists every mailbox the signed-in user can access. This server supports shared/delegated mailboxes by:

  1. Targeting — pass mailbox: "shared@contoso.com" on list/search/read/send/folder/thread tools. Calls use users/{upn}/... instead of me/..., plus an X-AnchorMailbox header.
  2. Discoverylist-mailboxes merges primary + OUTLOOK_SHARED_MAILBOXES + local cache + optional candidates[], then probes read/folder access. sendAs stays unverified until a successful send.
  3. Exchange rights — Graph scopes are not enough. The user still needs Full Access / Send As / Send on Behalf on the mailbox in Exchange Online.
  4. Send — default when mailbox is set: POST /users/{mailbox}/sendMail. Set onBehalfOf: true to use me/sendMail with from set to the shared address.

Message IDs are mailbox-scoped. Always pass the same mailbox value on follow-up read/thread calls.

Optional EXO admin seed (outside this MCP):

Get-EXOMailbox -RecipientTypeDetails SharedMailbox -ResultSize Unlimited |
  ForEach-Object {
    Get-EXOMailboxPermission -Identity $_.Identity -User $user -ErrorAction SilentlyContinue
  }

Pipe known addresses into OUTLOOK_SHARED_MAILBOXES.

MCP Client Configuration

Claude Desktop

Add to claude_desktop_config.json:

{
  "mcpServers": {
    "outlook": {
      "command": "node",
      "args": ["/path/to/outlook-mcp/index.js"],
      "env": {
        "MS_CLIENT_ID": "your-client-id-here"
      }
    }
  }
}

Hermes Agent

Add to config.yaml under mcp_servers:

mcp_servers:
  outlook-mcp:
    command: node
    args:
    - /path/to/outlook-mcp/index.js
    env:
      MS_CLIENT_ID: your-client-id-here

Multi-Account (Multiple Instances)

The server is single-account by design — getCachedAccount() always returns the first cached account. To use multiple accounts (e.g. work + personal), run separate instances with isolated token caches:

mcp_servers:
  outlook-mcp-work:
    command: node
    args: [/path/to/outlook-mcp/index.js]
    env:
      MS_CLIENT_ID: your-client-id
  outlook-mcp-personal:
    command: node
    args: [/path/to/outlook-mcp/index.js]
    env:
      MS_CLIENT_ID: your-client-id
      OUTLOOK_TOKEN_STORE_PATH: /path/to/.outlook-mcp-tokens-personal.json

Each instance gets its own MSAL cache — no account-selection conflicts.

Tools Reference

The server exposes 22 tools across six categories (21 when OUTLOOK_ENABLE_SHARED_MAILBOXES=false).

Authentication (3 tools)

Tool Description
about Returns server name and version
authenticate Starts MSAL device code flow — returns a URL and code for the user to complete sign-in
check-auth-status Checks if a valid cached token exists (no side effects)

Email (6 tools)

Tool Key Parameters Description
list-emails folder, count, dateFrom, dateTo, dateRange, mailbox Lists emails from a folder with date filtering. Results include conversationId. Pass mailbox to target a shared mailbox.
search-emails query, from, to, subject, hasAttachments, unreadOnly, count, strict, mailbox Progressive search with KQL fallback strategies. Results include conversationId. Pass mailbox for shared mailbox search. Use strict: true to disable fallback to recent emails.
read-email id, mailbox Reads a single email with full body (auto-cleaned HTML → text). Pass the same mailbox used when listing/searching.
read-emails ids (max 10), mailbox Reads multiple emails concurrently
send-email to, cc, bcc, subject, body, importance, saveToSentItems, mailbox, from, onBehalfOf Sends an email (plain text or HTML). When mailbox is set, sends as that shared mailbox via users/{mailbox}/sendMail (requires Send As + Mail.Send.Shared). Set onBehalfOf: true for Send on Behalf via me/sendMail with from set.
get-email-thread conversationId (preferred) or ids (max 20), mailbox Fetches a complete conversation across all folders (inbox + sent), strips quoted replies, deduplicates signatures per sender. Pass mailbox when the conversation is in a shared mailbox.

Date filtering supports both ISO dates (2024-06-15) and relative ranges (today, yesterday, last7days, last30days, thisweek, lastweek, thismonth, lastmonth).

Search strategy: The server uses a progressive fallback approach because Graph API doesn't allow $search with $orderby or $filter. It tries: (1) combined KQL search with client-side boolean filtering, (2) individual term searches, (3) boolean-filter-only with $filter + $orderby, (4) fallback to recent emails.

Calendar (6 tools)

Tool Key Parameters Description
list-events count, startDate, endDate Lists events in a date range (defaults to upcoming from now)
create-event subject, start, end, attendees, body Creates a calendar event (times in ISO 8601, timezone configurable via MS_TIMEZONE)
accept-event eventId, comment Accepts a meeting invitation
decline-event eventId, comment Declines a meeting invitation
cancel-event eventId, comment Cancels an event you organized
delete-event eventId Deletes an event from your calendar

Folders (3 tools)

Tool Key Parameters Description
list-folders includeItemCounts, includeChildren, mailbox Lists mail folders (flat list or hierarchical tree). Pass mailbox for a shared mailbox's folders.
create-folder name, parentFolder, mailbox Creates a new mail folder (optionally nested under a parent). There is no delete-folder tool — avoid test folders on shared mailboxes.
move-emails emailIds, targetFolder, sourceFolder, mailbox Moves emails to a target folder by name within the same mailbox

Inbox Rules (3 tools)

Tool Key Parameters Description
list-rules includeDetails Lists inbox rules sorted by execution order (sequence number)
create-rule name, fromAddresses, containsSubject, hasAttachments, moveToFolder, markAsRead, isEnabled, sequence Creates a new inbox rule
edit-rule-sequence ruleName, sequence Changes the execution order of an existing rule

Mailbox (1 tool, hidden when OUTLOOK_ENABLE_SHARED_MAILBOXES=false)

Tool Key Parameters Description
list-mailboxes candidates Probes primary mailbox + OUTLOOK_SHARED_MAILBOXES seeds + local cache + optional candidates[] for read/folder access. Reports sendAs as unverified until a successful send. Graph cannot enumerate all mailboxes the user has rights to.

Architecture

index.js                    # Main entry point — MCP server, tool registration, request routing
config.js                   # Server config, Graph API endpoints, field selections, pagination limits
.env.example                # Template for environment variables

auth/
  index.js                  # ensureAuthenticated() — the gatekeeper all tools call
  token-manager.js          # MSAL PublicClientApplication, device code flow, persistent cache plugin
  tools.js                  # authenticate, check-auth-status, about tool handlers

email/
  index.js                  # Tool definitions (schemas) for email tools
  list.js                   # list-emails handler
  search.js                 # search-emails handler with progressive KQL fallback
  read.js                   # read-email handler
  read-multiple.js          # read-emails handler (concurrent fetch)
  send.js                   # send-email handler
  folder-utils.js           # resolveFolderPath(), getFolderIdByName(), getAllFolders() — shared utilities

calendar/
  index.js                  # Tool definitions for calendar tools
  list.js                   # list-events handler
  create.js                 # create-event handler
  decline.js                # decline-event handler
  cancel.js                 # cancel-event handler
  delete.js                 # delete-event handler
  accept.js                 # accept-event handler

folder/
  index.js                  # Tool definitions for folder tools
  list.js                   # list-folders handler (flat + hierarchical formatting)
  create.js                 # create-folder handler
  move.js                   # move-emails handler

mailbox/
  index.js                  # Tool definition for list-mailboxes
  list.js                   # list-mailboxes handler — probes primary, seeds, cache, candidates

rules/
  index.js                  # Tool definitions for rules tools + edit-rule-sequence handler
  list.js                   # list-rules handler + getInboxRules() utility
  create.js                 # create-rule handler

tools/
  get-email-thread.js       # get-email-thread tool — conversationId-based thread fetcher

utils/
  graph-api.js              # callGraphAPI() — HTTPS client for Microsoft Graph (429/503 retry/backoff)
  mailbox.js                # normalizeMailbox(), buildPath(), withMailboxHeaders() — shared mailbox routing
  bodyParser.js             # HTML → text conversion, boilerplate/banner/legal block stripping, signature handling
  threadBuilder.js          # Quote-boundary detection, unique-content extraction, chronological thread formatting
  time-formatter.js         # Timezone-aware timestamp formatting for email/calendar display
  timezone-mapper.js        # Maps Windows timezone names to IANA for Intl-based date operations
  odata-helpers.js          # OData filter building, date parsing, relative date ranges
  mock-data.js              # Test mode mock responses

Key Design Decisions

Token persistence: MSAL's cache plugin serializes/deserializes the token cache to disk on every access, so tokens survive process restarts. The cache file is plain JSON but contains MSAL's internal format — don't edit it manually.

Body cleaning: All email bodies pass through bodyParser.cleanBody() which: (1) converts HTML to text, (2) decodes HTML entities, (3) strips external-email caution banners, (4) strips legal boilerplate (DISCLAIMER / CONFIDENTIALITY NOTICE blocks), (5) optionally strips signature blocks (Outlook id="Signature" divs or text delimiters).

Thread reconstruction: get-email-thread fetches all messages matching a conversationId across all folders (inbox + sent items), sorts chronologically in memory (because $filter + $orderby on conversationId causes a Graph API 400), strips quoted replies using pattern matching (Outlook-style "From:/Sent:" headers, Gmail-style "On [date] [name] wrote:", >-prefixed lines, forwarded message blocks), and deduplicates signatures per sender (first occurrence keeps signature, repeats strip it).

Progressive search: Graph API doesn't allow $search with $orderby or $filter. The search handler tries multiple strategies in order and applies boolean filters (unread, attachments) client-side when needed.

No external HTTP framework: graph-api.js uses Node's native https module directly — no axios, no node-fetch, no dependencies beyond MSAL and the MCP SDK.

Development

Test Mode

npm run test-mode

Sets USE_TEST_MODE=true — the server returns mock data for all API calls instead of hitting Graph API. Useful for testing tool schemas and response formatting without a real Microsoft account.

MCP Inspector

npm run inspect

Launches the MCP Inspector — a web UI for interactively testing MCP tools.

Security Notes

  • Token cache: The .outlook-mcp-tokens.json file contains access and refresh tokens. It's in .gitignore by default. Protect it like a password.
  • No client secret in code: For public client apps, no secret is needed. If you use a confidential client app, set MS_CLIENT_SECRET in the environment (not in the code).
  • Scopes are conditional: The OAuth scopes are defined in config.js. When OUTLOOK_ENABLE_SHARED_MAILBOXES is not false, the scope set includes Mail.Read.Shared, Mail.ReadWrite.Shared, and Mail.Send.Shared. Disable shared mailboxes to request a narrower scope set.
  • .gitignore covers: node_modules/, .env / .env.*, *.pem, *.key, *.cert, *.token.json, .outlook-mcp-tokens.json

Known Issues

  1. authenticate force parameter is a no-op — The force parameter is declared in the tool schema but never read by the handler. Calling authenticate(force=false) will still start a fresh device code flow. Use check-auth-status to check auth state without side effects.

License

MIT