{"id":"4a7296c3-0494-4b2c-ab49-07ba66168271","slug":"pepebits-outlook-mcp","kind":"mcp","name":"outlook-mcp","summary":"MCP server that gives Claude and other MCP clients access to your Outlook mailbox through Microsoft Graph","owner":"Pepebits","repoUrl":"https://github.com/Pepebits/outlook-mcp","license":"MIT","licenseTier":"permissive","trust":"community","stars":0,"match":null,"fewStars":true,"format":"mcp","createdAt":"2026-10-08T08:32:32.000Z","addedAt":"2026-10-14T05:17:10.668Z","logoUrl":"https://avatars.githubusercontent.com/u/82172692?v=4","linkOnly":false,"collection":null,"skillPath":null,"categories":["productivity","cloud-infra","memory-knowledge"],"tags":["claude","email","microsoft-graph","outlook"],"runtime":"node","local":true,"cloudCapable":false,"cloudCapability":"unknown","cloudReadyOnPlatform":false,"needsApiKey":false,"reviewed":false,"syncedAt":"2026-10-17T13:55:48.852Z","sample":false,"url":"https://conduits.dev/tools/pepebits-outlook-mcp","language":"TypeScript","transports":["stdio"],"install":{"npm":"@pepebits/outlook-mcp"},"requirements":{"env":[{"name":"OUTLOOK_CLIENT_ID","secret":false,"required":false,"description":"Your own Azure app (client) ID. Leave unset to use the shared app for personal accounts."},{"name":"OUTLOOK_TENANT","secret":false,"required":false,"description":"consumers (default), organizations, common or a tenant ID."},{"name":"OUTLOOK_READ_ONLY","secret":false,"required":false,"description":"Set to true to expose only read-only tools."},{"name":"OUTLOOK_TOKEN_STORE","secret":false,"required":false,"description":"auto (default), keychain or file: where the token cache is stored."}],"upstreamMayCharge":false},"tools":null,"readme":"<p align=\"center\"><img src=\"https://raw.githubusercontent.com/Pepebits/outlook-mcp/main/docs/banner.png\" alt=\"outlook-mcp\" width=\"720\"></p>\n\nA [Model Context Protocol](https://modelcontextprotocol.io) server that lets Claude, Codex and any other MCP client read, search, send and organize your **Outlook / Microsoft 365 / Outlook.com** mail through the Microsoft Graph API.\n\n---\n\n## 🔭 Overview\n\n`outlook-mcp` runs locally over stdio. It signs in with the **OAuth device code flow** (no client secret, no redirect URI), stores the refresh token in your OS keychain (or a local file only you can read), and talks to Microsoft Graph with plain `fetch`.\n\n## ✨ Features\n\n- 📂 Browse folders, list and search messages (KQL), read bodies as sanitized plain text\n- 📎 List and download attachments safely into a configured directory\n- ✉️ Send mail, create drafts, reply, reply-all and forward (local attachments up to 150 MB; files of 3 MB or more are uploaded in chunks)\n- 🗂️ Move, mark read/unread, flag and delete messages, one at a time or in bulk (`ids`)\n- 📰 Find newsletters, unsubscribe from them and block unwanted senders with inbox rules\n- 🔒 Read-only mode that removes every mutating tool\n- 🔁 Automatic retry with `Retry-After` / exponential backoff for throttling (429/503/504)\n- 🧾 Logs only to stderr; never logs tokens or message bodies\n\n## 📋 Requirements\n\n- Node.js **24 (LTS) or newer**\n- A Microsoft account (personal works out of the box; work or school needs [your own Azure app](#️-use-your-own-azure-app-optional))\n\n## 🚀 Quick start\n\nNo terminal sign-in and no Azure setup needed. You only need Node.js 24+.\n\n### Claude Code\n\n```bash\nclaude mcp add outlook --scope user -- npx -y @pepebits/outlook-mcp\n```\n\n### Claude Desktop\n\nAdd to `claude_desktop_config.json` and restart Claude Desktop:\n\n```json\n{\n  \"mcpServers\": {\n    \"outlook\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"@pepebits/outlook-mcp\"]\n    }\n  }\n}\n```\n\n### Codex (OpenAI)\n\n```bash\ncodex mcp add outlook -- npx -y @pepebits/outlook-mcp\n```\n\nOr add it to `~/.codex/config.toml` (shared by the Codex CLI and IDE extension):\n\n```toml\n[mcp_servers.outlook]\ncommand = \"npx\"\nargs = [\"-y\", \"@pepebits/outlook-mcp\"]\n# env = { OUTLOOK_READ_ONLY = \"true\" }\n```\n\nRun `/mcp` inside Codex to check that it is connected.\n\n### Other MCP clients\n\nAny client that can launch a stdio MCP server works: run `npx -y @pepebits/outlook-mcp` as the server command.\n\n### Sign in\n\nAsk your assistant to *\"log in to Outlook\"*. The `login` tool returns a URL and a one-time code: open the URL, enter the code and accept. Prefer a terminal? Run `npx -y @pepebits/outlook-mcp auth` instead.\n\nBy default the server uses the shared **outlook-mcp** Azure app, which supports **personal Microsoft accounts** (outlook.com, hotmail, live). The consent screen may show an *unverified publisher* warning. No data passes through any server of ours: the server talks to Microsoft Graph directly from your machine and your tokens stay in the local token cache. For work or school accounts, or to use your own app, see [Use your own Azure app](#️-use-your-own-azure-app-optional).\n\n## ⚙️ Configuration (`.env`)\n\nThe `.env` file is optional. Variables are read from `.env` in the current directory and in the package directory, and real environment variables override it. You can also pass them through your MCP client's env block, e.g. `claude mcp add outlook --scope user -e OUTLOOK_TENANT=organizations -e OUTLOOK_CLIENT_ID=your-client-id -- npx -y @pepebits/outlook-mcp`.\n\n| Variable | Default | Description |\n| --- | --- | --- |\n| `OUTLOOK_CLIENT_ID` | shared outlook-mcp app | Application (client) ID of your own Azure app registration (optional). |\n| `OUTLOOK_TENANT` | `consumers` | Authority tenant: `consumers` (personal), `organizations` (work/school), `common` (both) or a tenant GUID. |\n| `OUTLOOK_SCOPES` | `User.Read Mail.ReadWrite Mail.Send MailboxSettings.ReadWrite offline_access` | Space-separated delegated Graph scopes requested at sign-in. |\n| `OUTLOOK_TOKEN_STORE` | `auto` | Where the token cache lives: `auto` (OS keychain when available, else file), `keychain` (fails if no keychain) or `file`. See [Token storage](#️-token-storage). |\n| `OUTLOOK_TOKEN_CACHE` | `~/.config/outlook-mcp/token-cache.json` | Token cache file (mode `0600`) for the `file` store; also identifies the keychain entry. |\n| `OUTLOOK_READ_ONLY` | `false` | When `true`, send/move/delete/flag/mark tools are not registered at all. |\n| `OUTLOOK_DOWNLOAD_DIR` | `~/Downloads` | The only directory attachments may be written to. |\n| `OUTLOOK_DEFAULT_TOP` | `20` | Default page size for list/search tools (1-100). |\n| `OUTLOOK_MAX_BODY_CHARS` | `20000` | Maximum body characters returned by `get_message`. |\n| `OUTLOOK_GRAPH_BASE_URL` | `https://graph.microsoft.com/v1.0` | Graph endpoint (change for national clouds). |\n| `LOG_LEVEL` | `info` | `debug`, `info`, `warn` or `error` (written to stderr). |\n\n## 🔐 Authentication\n\n```bash\nnpx -y @pepebits/outlook-mcp auth      # device code sign-in; prints a URL and a code\nnpx -y @pepebits/outlook-mcp whoami    # silent token + GET /me\nnpx -y @pepebits/outlook-mcp logout    # removes accounts and deletes the token cache (keychain entry and file)\n```\n\nYou can also sign in without a terminal. Just ask your assistant to *\"log in to Outlook\"*:\n\n- 🔑 `login` returns a URL and a one-time code. Open the URL, enter the code and accept; sign-in finishes in the background.\n- ✅ `auth_status` tells you whether you are signed in, still waiting, or signed out.\n- 🚪 `logout` removes the cached account and the token cache (keychain entry and file).\n\nUse `login` with `force: true` to sign in again, for example after adding a permission in Azure. If the session is missing or expired, tools return *\"Not signed in or session expired. Call the login tool ...\"*.\n\n## 🗝️ Token storage\n\nThe refresh token is the most sensitive thing this server holds, so by default (`OUTLOOK_TOKEN_STORE=auto`) it goes into the operating system keychain instead of a plain file. No native npm dependencies are used; the server calls the OS tools directly:\n\n| Platform | Backend | Notes |\n| --- | --- | --- |\n| macOS | Keychain via the `security` CLI | Service `outlook-mcp`, account = the cache path. |\n| Linux | Secret Service via `secret-tool` (libsecret) | Used only if `secret-tool` is installed and a keyring is running; otherwise the file store is used. |\n| Windows and others | File | `~/.config/outlook-mcp/token-cache.json`, mode `0600`. |\n\n- **Migration:** if a cache file exists and the keychain is still empty, it is imported into the keychain and the file is deleted.\n- **Fallback:** in `auto` mode, if the keychain cannot be used at runtime the server falls back to the file and logs a warning. `OUTLOOK_TOKEN_STORE=keychain` never falls back and fails instead; `file` never touches the keychain.\n- **Secrets stay out of process listings:** on Linux `secret-tool` reads the secret from stdin. `security add-generic-password` only accepts the password as a command-line argument (visible in `ps` to other local processes), so on macOS the commands are fed to `security -i` over stdin instead. That interface truncates lines at about 4 KB, so the cache is stored base64 encoded across a few small keychain items (`<account>#0`, `#1`, ...).\n- `logout` (tool and CLI) removes the keychain entry as well as the file.\n- On macOS the first access from a different binary may show a Keychain prompt; click *Always Allow*.\n\n## 🛠️ Use your own Azure app (optional)\n\nBy default `outlook-mcp` uses the shared **outlook-mcp** Azure app, which works with personal Microsoft accounts only. Register your own app if you need **work or school** accounts or simply prefer to use your own. Then set `OUTLOOK_CLIENT_ID` (and `OUTLOOK_TENANT`, see below).\n\n### Step by step\n\n1. Open [portal.azure.com](https://portal.azure.com) and go to **Microsoft Entra ID** -> **App registrations** -> **New registration**.\n2. Give it a name (for example `outlook-mcp`).\n3. Under **Supported account types** choose:\n   - **Personal Microsoft accounts only** (Outlook.com, Hotmail, Live), or\n   - **Accounts in any organizational directory and personal Microsoft accounts** (both).\n   - For work/school only, pick an organizational option.\n4. Leave **Redirect URI** empty and click **Register**.\n5. In the left menu go to **Manage** -> **Authentication** -> **Settings** tab, set **Allow public client flows** to **Yes** and save.\n6. Go to **Manage** -> **API permissions**. `User.Read` is already granted by default. Click **Add a permission** -> **Microsoft Graph** -> **Delegated permissions** and add:\n   - 📬 Expand the **Mail** group and tick `Mail.ReadWrite` and `Mail.Send`.\n   - ⚙️ Expand the **MailboxSettings** group and tick `MailboxSettings.ReadWrite` (needed for inbox rules and `block_sender`).\n   - 🔑 Expand the **OpenId permissions** group and tick `offline_access`.\n\n   Then click **Add permissions**. No admin consent is needed for personal accounts.\n\n| Scope | Used for |\n| --- | --- |\n| `User.Read` | Sign-in and `whoami` |\n| `Mail.ReadWrite` | Reading, searching, moving, flagging and deleting messages |\n| `Mail.Send` | `send_mail`, replies, forwards and mailto unsubscribe |\n| `MailboxSettings.ReadWrite` | Inbox rules: `list_rules`, `create_rule`, `delete_rule`, `block_sender` |\n| `offline_access` | Refresh token, so you only sign in once |\n\n> 💡 If you upgrade from 0.1.0, add `MailboxSettings.ReadWrite` in Azure and run `login` with `force: true` (or `npx -y @pepebits/outlook-mcp auth`) again.\n\n> 💡 The Azure portal may be shown in your language, so labels can differ slightly (e.g. *Administrar* -> *Autenticación* -> *Configuración*, *Permisos de OpenId*).\n7. From the **Overview** page copy the **Application (client) ID**. This is your `OUTLOOK_CLIENT_ID`.\n\n> 🏢 **Work or school accounts:** set `OUTLOOK_CLIENT_ID` to your app and `OUTLOOK_TENANT=organizations` (or your tenant GUID). Your organization may require **admin consent** for the mail permissions.\n\n## 🧰 Tools reference\n\n| Tool | Mutating | Description |\n| --- | :---: | --- |\n| `login` | no | Start a device code sign-in; returns the URL and code (`force` to sign in again). |\n| `auth_status` | no | Signed in, pending sign-in or signed out. |\n| `logout` | no | Sign out and delete the local token cache. |\n| `list_folders` | no | List mail folders or child folders (`parentFolderId`, `includeHidden`). |\n| `list_messages` | no | List messages, newest first, with filters (`unreadOnly`, `from`, `since`, `until`, `hasAttachments`) and `nextLink` paging. |\n| `search_messages` | no | KQL full-text search with `nextLink` paging. |\n| `get_message` | no | Full message with recipients, flags and body (`format` text/html, `maxChars`). |\n| `list_attachments` | no | Attachment metadata for a message. |\n| `download_attachment` | no | Save an attachment into `OUTLOOK_DOWNLOAD_DIR` (writes locally only; no overwrite unless `overwrite: true`). |\n| `send_mail` | yes | Send an email, optionally with local attachments up to 150 MB (3 MB or more go through an upload session; if an upload fails nothing is sent). |\n| `create_draft` | yes | Create a draft without sending. Attachments up to 150 MB; failed uploads are reported and the draft is kept. |\n| `reply_message` | yes | Reply or reply-all (`replyAll`). |\n| `forward_message` | yes | Forward to new recipients. |\n| `move_message` | yes | Move to a folder; returns the new id. Accepts `id` or `ids` (up to 50). |\n| `mark_read` | yes | Mark read or unread. Accepts `id` or `ids` (up to 50). |\n| `flag_message` | yes | `flagged`, `complete` or `notFlagged`. Accepts `id` or `ids` (up to 50). |\n| `delete_message` | yes | Move to Deleted Items, or `permanent: true` to delete irreversibly. Accepts `id` or `ids` (up to 50). |\n| `bulk_action` | yes | Apply `delete`, `move`, `archive`, `markRead`, `markUnread`, `flag` or `unflag` to every message matching a `query` (KQL) or filters (`folderId`, `from`, `since`, `until`, `unreadOnly`, `hasAttachments`). `dryRun` is on by default; `max` defaults to 100 (hard cap 500). |\n| `get_unsubscribe_info` | no | Show how to leave the mailing list a message came from (List-Unsubscribe). |\n| `unsubscribe` | yes | Leave a mailing list: RFC 8058 one-click, else a mailto request, else returns the link. Accepts `id` or `ids` (up to 50). |\n| `find_newsletters` | no | Scan a folder (`folderId`, `maxMessages` up to 2000, `excludeDomains`, `includeNoUnsubscribe`), group by sender and report mailing lists with their unsubscribe method, most frequent first. |\n| `mail_digest` | no | What is waiting in a folder (`folderId`, `since` default 24h, `unreadOnly` default true, `maxMessages` up to 1000, `groupBy` sender/none): counts, high-importance and flagged messages, and senders grouped with a newsletter hint. |\n| `list_rules` | no | List inbox rules. |\n| `create_rule` | yes | Create an inbox rule from `fromAddresses` / `senderContains` / `subjectContains` with action `move`, `delete`, `markRead` or `junk`. |\n| `delete_rule` | yes | Delete an inbox rule by id. |\n| `block_sender` | yes | Block `addresses` and/or `domains` with a rule that moves their future mail to Deleted Items. |\n\n**Bulk operations:** the tools marked \"Accepts `id` or `ids`\" take exactly one of the two. With `ids` they process up to 50 messages (a few at a time), never stop at the first error and return `{ results: [{ id, ok, ... | error }], succeeded, failed }`. With `id` the response is unchanged.\n\nWell-known folder names accepted anywhere a folder id is expected: `inbox`, `drafts`, `sentitems`, `deleteditems`, `junkemail`, `archive`.\n\n## 📬 Daily digest\n\nAsk *\"What's waiting in my inbox today?\"* and the assistant calls `mail_digest`: it scans the unread mail of the last 24 hours (adjust with `since`, `unreadOnly`, `maxMessages`) and returns counts (total, unread, flagged, high importance, with attachments), the high-importance and flagged messages, and the messages grouped by sender with their latest subject and a few message ids. For the top 20 senders it checks the `List-Unsubscribe` header, so newsletters are marked with `isNewsletter` and can be handed straight to `unsubscribe`.\n\n## 🧹 Cleaning up your inbox\n\nAsk Claude something like *\"Find the newsletters cluttering my inbox, unsubscribe from the ones I don't read and block the rest\"*. Behind the scenes:\n\n1. `find_newsletters` scans the inbox and lists senders with a `List-Unsubscribe` header, most frequent first, each with a `sampleMessageId` and an `unsubscribe` method (`one-click`, `mailto` or `link`).\n2. `unsubscribe` with `ids` set to the chosen sample message ids leaves those lists. Senders that only offer a link come back with the URL to open in a browser.\n3. `block_sender` with `addresses` or `domains` creates an inbox rule that sends their future mail to Deleted Items. Use `delete_message` with `ids` to clear what is already in the inbox, and `list_rules` / `delete_rule` to review or undo a block.\n\nInbox rules need the `MailboxSettings.ReadWrite` permission (see the Azure steps above). Do not unsubscribe from spam or phishing: it confirms your address is active. Block those senders instead.\n\n## 🔎 Bulk clean-up by search\n\nAsk *\"Archive everything from newsletters@shop.com received before 2025\"*. `bulk_action` always starts as a **dry run**: it changes nothing and returns the match count plus a sample of 10 messages.\n\n```json\n{ \"from\": \"newsletters@shop.com\", \"until\": \"2025-01-01\", \"action\": \"archive\" }\n```\n\nConfirm the count with the assistant, then it calls the tool again with `dryRun: false` to apply the action:\n\n```json\n{ \"from\": \"newsletters@shop.com\", \"until\": \"2025-01-01\", \"action\": \"archive\", \"dryRun\": false }\n```\n\nUse either `query` (KQL, e.g. `from:shop.com hasattachments:true`) or the filter fields, not both. At most `max` messages are processed (default 100, hard cap 500) and the response says if the result was `truncated`; run it again for the rest. `delete` moves to Deleted Items (recoverable), `archive` moves to the Archive folder, and `move` needs `destinationFolderId`. Failures never stop the run: the response lists each id with `ok` or `error`.\n\n## 👁️ Read-only mode\n\nSet `OUTLOOK_READ_ONLY=true` to register only the non-mutating tools. The mutating tools do not exist for the client, so they cannot be called at all. Combine it with a token cache created using only `User.Read Mail.Read` (set `OUTLOOK_SCOPES` accordingly and re-run `npx -y @pepebits/outlook-mcp auth`) for defense in depth.\n\n## 🛡️ Security\n\n- 🔑 The token cache is stored in the OS keychain when available (macOS Keychain, Linux Secret Service; see [Token storage](#️-token-storage)), otherwise in a local file with mode `0600` in a `0700` directory, written atomically.\n- 🙅 No client secret exists: this is a public client using device code flow.\n- 🤐 Tokens and message bodies are never logged; MSAL PII logging is disabled.\n- 📥 Attachment downloads are confined to `OUTLOOK_DOWNLOAD_DIR`; path traversal is rejected.\n- ⚠️ `delete_message` with `permanent: true` is **irreversible**, and `send_mail`, `reply_message` and `forward_message` send immediately. Prefer read-only mode or `create_draft` when in doubt.\n- 🧠 Email content is untrusted input: a malicious message may try to instruct the model (prompt injection). To help the model tell data from instructions, results from `get_message`, `list_messages`, `search_messages`, `list_attachments` and `find_newsletters` group sender-controlled text (subjects, senders, previews, attachment names) under an `untrusted` key and carry a `note` saying so. The `get_message` body is enclosed in `<untrusted_email_content>` markers, and any marker found inside the content is neutralized so it cannot close the wrapper early.\n- 🤝 The server also sends MCP `instructions` telling clients that email content is untrusted and that sending, deleting, unsubscribing and rule changes should be confirmed with the user. These are mitigations, not guarantees: keep a human in the loop for those actions or use read-only mode.\n\n## 🩺 Troubleshooting\n\n| Symptom | Fix |\n| --- | --- |\n| `AADSTS7000218` (client assertion / secret required) | Enable **Allow public client flows** in Azure -> Manage -> Authentication -> Settings. |\n| `AADSTS50020` or wrong tenant | The account type does not match `OUTLOOK_TENANT`. Use `consumers` for personal, `organizations` for work/school, `common` for both, and make sure the app registration supports that account type. |\n| \"Not signed in or session expired\" | Ask the assistant to call `login`, or run `npx -y @pepebits/outlook-mcp auth`. |\n| Search returns \"Invalid search query\" | `search_messages` uses KQL, e.g. `from:alice subject:\"report\" hasattachments:true`. Graph returns at most about 250 results per search, and results are not sorted. |\n| \"Message not found\" after a move | Message ids change when a message is moved. Use the `newId` returned by `move_message` or list the folder again. |\n| \"Access denied for inbox rules\" | Add `MailboxSettings.ReadWrite` in Azure -> API permissions, make sure it is in `OUTLOOK_SCOPES`, then run `npx -y @pepebits/outlook-mcp auth` again. |\n| Access denied / consent required | Add the missing delegated permission, or ask an admin for consent. |\n| Server does not start in a client | Check stderr logs; if you use your own app, `OUTLOOK_CLIENT_ID` must be set via `.env` or the client's `env` block. |\n\n## 🧑‍💻 Development\n\n```bash\ngit clone https://github.com/Pepebits/outlook-mcp.git\ncd outlook-mcp\nnpm install\ncp .env.example .env        # optional\nnpm run build\nnpm test\n```\n\nRun it from source in your MCP client:\n\n```bash\nclaude mcp add outlook --scope user -- node /absolute/path/outlook-mcp/dist/index.js\n```\n\nUse `npm run inspect` to try the tools in the MCP Inspector.\n\n### 🚢 Releasing (maintainers)\n\n1. Move the `[Unreleased]` notes in `CHANGELOG.md` under the new version, bump the version in `package.json` and `server.json` (top level and package), commit and push.\n2. Tag it: `git tag -a vX.Y.Z -m \"Short summary\" && git push origin vX.Y.Z`. The Release workflow runs the tests, **stages** the version on npm (Trusted Publishing, with provenance) and creates the GitHub release.\n3. Approve the staged version on npmjs.com (or `npm stage approve <stage-id>`), which needs your 2FA.\n4. The workflow waits for the approval, lists the version in the MCP Registry and announces the release on Telegram. If you approve more than ~6 hours later, the wait times out: run the Release workflow by hand (**Actions → Release → Run workflow**) with the tag to announce it.\n\n## 🗺️ Roadmap\n\nThis server is about mail only; calendar and other Microsoft 365 data are out of scope.\n\nThere is nothing planned right now. Ideas are welcome: [open an issue](https://github.com/Pepebits/outlook-mcp/issues).\n\n## 🤝 Contributing\n\nIssues and pull requests are welcome.\n\n```bash\nnpm install\nnpm run typecheck\nnpm test\nnpm run build\n```\n\nPlease keep code, comments and docs in English, add tests for new behavior, and never log secrets or message content.\n\n## 📄 License\n\n[MIT](LICENSE)\n"}