| auth | ||
| calendar | ||
| folder | ||
| rules | ||
| tests | ||
| tools | ||
| utils | ||
| .env.example | ||
| .gitignore | ||
| config.js | ||
| index.js | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
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
- 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.
- Go to Azure Portal → App Registrations → New registration
- Name it (e.g. "Outlook MCP")
- Supported account types: Accounts in any organizational directory and personal Microsoft accounts (multi-tenant)
- Redirect URI: leave blank (device code flow doesn't need one)
- After creation, go to Authentication → scroll to Advanced settings → enable Allow public client flows
- Go to API permissions → add Delegated Microsoft Graph permissions:
Mail.ReadMail.ReadWriteMail.SendUser.ReadCalendars.ReadCalendars.ReadWriteMailboxSettings.ReadWriteoffline_access
- Copy the Application (client) ID — that's your
MS_CLIENT_ID
No client secret is needed for public client apps.
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 | Eastern Standard Time |
Default timezone for calendar event creation (IANA or Windows timezone name) |
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 21 tools across five categories.
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 |
Lists emails from a folder with date filtering. Results include conversationId. |
search-emails |
query, from, to, subject, hasAttachments, unreadOnly, count |
Progressive search with KQL fallback strategies. Results include conversationId. |
read-email |
id |
Reads a single email with full body (auto-cleaned HTML → text) |
read-emails |
ids (max 10) |
Reads multiple emails concurrently |
send-email |
to, cc, bcc, subject, body, importance, saveToSentItems |
Sends an email (plain text or HTML) |
get-email-thread |
conversationId (preferred) or ids (max 20) |
Fetches a complete conversation across all folders (inbox + sent), strips quoted replies, deduplicates signatures per sender |
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 |
Lists mail folders (flat list or hierarchical tree) |
create-folder |
name, parentFolder |
Creates a new mail folder (optionally nested under a parent) |
move-emails |
emailIds, targetFolder, sourceFolder |
Moves emails to a target folder by name |
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 |
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
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
bodyParser.js # HTML → text conversion, boilerplate/banner/legal block stripping, signature handling
threadBuilder.js # Quote-boundary detection, unique-content extraction, chronological thread formatting
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.jsonfile contains access and refresh tokens. It's in.gitignoreby 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_SECRETin the environment (not in the code). - Scopes are hardcoded: The OAuth scopes are defined in
config.jsand include read/write for mail and calendar. Adjust if you need fewer permissions. .gitignorecovers:node_modules/,.env*,*.token.json,*.pem,*.key,.outlook-mcp-tokens.json
Known Issues
authenticateforceparameter is a no-op — Theforceparameter is declared in the tool schema but never read by the handler. Callingauthenticate(force=false)will still start a fresh device code flow. Usecheck-auth-statusto check auth state without side effects.
License
MIT