Bug fixes (non-breaking): - Register accept-event tool in calendar module (was dead code) - Add missing callGraphAPI + ensureAuthenticated imports to rules/index.js (edit-rule-sequence would throw ReferenceError at runtime) - Make calendar event timezone configurable via MS_TIMEZONE env var (was hardcoded to UTC; default is now Eastern Standard Time) Version bump: 1.0.0 → 1.0.1 README updated: 21 tools, MS_TIMEZONE in config table, known issues pruned
300 lines
No EOL
13 KiB
Markdown
300 lines
No EOL
13 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
|
|
- **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`
|
|
- `User.Read`
|
|
- `Calendars.Read`
|
|
- `Calendars.ReadWrite`
|
|
- `MailboxSettings.ReadWrite`
|
|
- `offline_access`
|
|
7. 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`:
|
|
|
|
```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 **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
|
|
|
|
```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 hardcoded**: The OAuth scopes are defined in `config.js` and include read/write for mail and calendar. Adjust if you need fewer permissions.
|
|
- **`.gitignore` covers**: `node_modules/`, `.env*`, `*.token.json`, `*.pem`, `*.key`, `.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 |