outlook-mcp-limited
outlook-mcp-limited
A local Model Context Protocol (MCP) server that gives an AI assistant
(Claude Desktop, Claude Code, Cursor, …) read + draft-only access to one personal Microsoft mailbox
(@live.nl, @outlook.com, @hotmail.com). It can search, list and read mail, list folders, save
attachments to a jailed folder, and create drafts. It cannot send, delete, move or mark mail, and it
never touches calendar, contacts or files.
This is a cut-down fork of xdarkoy/outlook-mcp (MIT). The fork removes six tools and one OAuth scope, and adds two hard-coded allowlists that make the server refuse to start if anyone widens them. See docs/decision-note.md for why this base was chosen.
It runs in two modes: stdio on your own machine (default, 6 tools), or hosted over Streamable HTTP behind a bearer token for cloud agents such as Cursor's Grok Bot (5 tools, no attachment saving). Hosted mode puts your refresh token and your mail content on other people's servers; read docs/hosting.md before you use it.
What it can and cannot do
Allowed (tool) | Forbidden (no tool, and no OAuth scope for it) |
| Send mail ( |
| Send an existing draft |
| Delete mail |
| Move or mark mail |
| Calendar, contacts, OneDrive, shared mailboxes |
| Anything as an application (no client secret, no app-only permissions) |
Security model
1. The OAuth token cannot send. The server requests exactly four delegated scopes, hard-coded in
src/auth/scopes.ts:
Scope | What Microsoft says it allows | Why we need it |
| Refresh tokens | Sign in once, not every hour. Refresh tokens last 90 days by default [9]. |
| Sign in and read the user's profile | Identify the signed-in account (MSA vs work) |
| "Read user mail" [5] | search, list, read, list folders, save attachments |
| "Create, read, update and delete email in user mailboxes" [5] |
|
Mail.Send is the only permission that lets a token call POST /me/sendMail [6] or
POST /me/messages/{id}/send [7]. It is not requested, so both calls fail with HTTP 403. The consent
screen you see at first login therefore never says "send mail as you". npm run smoke:live proves this
against your real mailbox (see Acceptance checklist).
2. The tool list cannot grow by accident. src/tools/registry.ts holds the
six allowed tool names. At startup main() runs both guards; on any extra, missing or duplicate tool, or
any scope outside the allowlist, the process prints refusing to start and exits 1 before it serves a
single request. The tests in scripts/test-policy.mjs and scripts/test-mail-trust-boundary.mjs pin
both lists a third time.
3. Residual risk you should know about. Mail.ReadWrite also covers update and delete [5]. This
server exposes no tool for that, but the token could do it. Anyone who steals
~/.outlook-mcp/cache.json can read your mail and edit or delete messages until you revoke consent.
Treat that file like a password.
4. Token cache. MSAL writes ~/.outlook-mcp/cache.json (override: OUTLOOK_MCP_CACHE_DIR) with
mode 0600, via atomic write-to-temp-and-rename. No cloud, no keychain, no telemetry. To wipe it:
rm -rf ~/.outlook-mcp5. Revoke. Go to https://microsoft.com/consent, sign in with the mailbox account, open the app and choose Remove these permissions [10]. Then delete the cache as above. Deleting the app registration in Entra also kills every token issued for it.
6. No client secret. The app registration is a public client using the device code flow [2]. Nothing secret is stored in this repo or on disk except the token cache itself.
7. Attachments are jailed. save_attachment only writes inside OUTLOOK_MCP_ALLOWED_DIR
(default ~/Downloads/outlook-mcp/), never overwrites, and rejects path traversal.
8. Hosted mode is locked and stateless. In http mode, initialize, ping and tools/list are
public metadata (tool names and descriptions, nothing else) so a hosting platform can register the
server. Every other request to /mcp must carry Authorization: Bearer <MCP_AUTH_TOKEN> (32+
characters, compared in constant time) and is rate limited (default 60/min). Each request gets a fresh
MCP server; nothing is kept between requests. The signed-in session comes from the
OUTLOOK_MCP_TOKEN_CACHE variable, which you create locally with npm run export-token. Without a
usable MCP_AUTH_TOKEN the server runs locked: mail calls answer 503 and GET /healthz reports
"status":"locked" plus which settings are present (never their values). Full runbook and the risks
you accept: docs/hosting.md.
Quick start (about 30 minutes, once)
Register the app in Microsoft Entra — follow docs/entra-setup.md. You end with an Application (client) ID. Personal accounts only, public client flows on, four delegated permissions, no secret.
Install and build (Node 20 or newer; this repo was built with Node 26):
cd /path/to/outlook-mcp npm install npm run buildSign in once (device code flow; prints a URL and a code):
OUTLOOK_MCP_CLIENT_ID=<your-client-id> npm run loginOpen the URL, enter the code, sign in with
you@live.nl, and read the consent screen: it must list only read/write mail, profile and offline access — not "send mail as you". Accept.Smoke test against the real mailbox:
OUTLOOK_MCP_CLIENT_ID=<your-client-id> npm run smoke:liveExpected: every line
PASS, including the twoAC5 … refusedlines withHTTP 403. The script leaves one draft addressed to yourself in Drafts; check it in Outlook web, then delete it by hand.Connect a client — see docs/wire-up-notes.md. Short version for Claude Desktop (
~/Library/Application Support/Claude/claude_desktop_config.jsonon macOS [13]):{ "mcpServers": { "outlook": { "command": "node", "args": ["/path/to/outlook-mcp/dist/index.js"], "env": { "OUTLOOK_MCP_CLIENT_ID": "<your-client-id>" } } } }
Configuration
Variable | Required | Default | Purpose |
| yes | — | Application (client) ID of your Entra app registration. Not a secret, but keep it out of git. |
| no |
| Authority tenant. |
| no |
| Where |
| no |
| Token cache location. |
| no |
| Hard cap before |
| http mode: yes | — | Shared secret clients must send as |
| http mode: yes | — | Base64 of the local token cache, from |
| no |
| Where http mode listens. Setting |
| no |
| Accepted requests per minute in http mode. |
| no | — | Set to |
Copy .env.example to .env if you prefer a file; nothing in this repo reads .env
automatically, so pass the values through your MCP client's env block or your shell.
Commands
node dist/index.js # MCP stdio server (what your local client launches)
node dist/index.js http # hosted Streamable HTTP server on $PORT (alias: npm run start:http)
node dist/index.js login # one-time device-code sign-in (alias: npm run login)
node dist/index.js export-token # token cache as base64 for hosting (alias: npm run export-token)
node dist/index.js help # help
npm test # build + all offline tests (no account needed)
npm run smoke:live # acceptance checks against the real mailbox (local)
npm run smoke:remote # acceptance checks against a deployed URL (MCP_URL + MCP_AUTH_TOKEN)dist/ is committed on purpose, so a host that only runs npm install && npm start works without a
build step. After changing src/, run npm run build and commit dist/ too (npm run check:dist
fails if you forget).
Acceptance checklist
# | Criterion | How to verify |
AC1 | No |
|
AC2 | No send tool in the MCP tool list |
|
AC3 | Search + read work on real mail |
|
AC4 |
|
|
AC5 | Graph send endpoints refuse this token |
|
AC6 | Non-expert finishes Entra + local run in ≤30 min | Follow Quick start; the runbook has one click per line |
AC7 | Calendar/contact/send tools absent |
|
AC8 |
| This file, |
H1 | Hosted |
|
H2 | Hosted tool list is five tools, no |
|
H3 | Hosted server stays locked (503 on |
|
Troubleshooting
Symptom | Cause / fix |
| "Allow public client flows" is off. Entra → your app → Authentication → Advanced settings → Yes → Save [3]. |
| Wrong authority. Keep |
| Device code rejected for the tenant alias. Try |
| The cache is empty or expired (90 days [9]). Run |
HTTP 403 on | A delegated permission is missing in the app registration, or consent was declined. Re-check API permissions, then |
| Someone edited the scope list or tool registry. That is the guard doing its job. |
Search returns hits from any year despite | Known MSA backend limitation; use |
Claude Desktop shows no tools | Quit and restart the app fully; check |
Development
npm run build # tsc → dist/
npm test # build + scripts/test-*.mjs + offline MCP smoke testTrust boundaries and the rules for changing them are in AGENTS.md. Upstream history and the fork changes are in CHANGELOG.md.
Sources
Configure desktop apps that call web APIs — enable public client flow
Using device code flow in MSAL.NET — Microsoft personal accounts
Managing apps and services connected to our Microsoft Accounts
License
MIT, same as upstream. See LICENSE.