349 lines
No EOL
18 KiB
Markdown
349 lines
No EOL
18 KiB
Markdown
# 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 <repo-url> 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 |