# Outlook MCP Server A [Model Context Protocol](https://modelcontextprotocol.io) 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](#azure-app-registration) below) ### Install ```bash git clone outlook-mcp cd outlook-mcp npm install ``` ### Configure ```bash cp .env.example .env # Edit .env and set MS_CLIENT_ID ``` ### Run ```bash 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 Registrations](https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps/ApplicationsListBlade) → **New 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. **Discovery** — `list-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): ```powershell 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`: ```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`: ```yaml 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: ```yaml 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 ```bash 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 ```bash npm run inspect ``` Launches the [MCP Inspector](https://modelcontextprotocol.io/docs/tools/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