Outlook assistant agent

MCP server for Outlook email, calendar, and contacts — let your AI assistant manage your inbox directly from the conversation.

by littlebearapps·MIT license·★ 39 Stars on the repo·GitHub ↗

Files of Outlook assistant

littlebearapps/main1 file
README.md
Show the full text656 lines

Outlook Assistant

Outlook Assistant

MCP server for Outlook email, calendar, and contacts — let your AI assistant manage your inbox directly from the conversation.

npm version npm downloads CI CodeQL License: MIT Glama score

Outlook Assistant connects AI assistants to your Microsoft Outlook account through the Model Context Protocol. Ask your AI assistant to search your inbox, send emails, schedule meetings, manage contacts, and configure mailbox settings — without leaving the conversation. Works with Claude, GitHub Copilot, Cursor, Windsurf, and any MCP-compatible client.

Works with personal Outlook.com and work/school Microsoft 365 accounts.


Outlook Assistant Demo — searching emails, reading, and drafting a reply
Search inbox → read & summarise → draft a reply — all from the conversation

What you can do
  • 📨 Search and read emails — find messages by sender, subject, date, or keywords; read full threads with conversation grouping; batch flag, move, export, or categorise multiple emails at once
  • 🛡️ Send emails with safety controls — dry-run preview, pre-send mail tips (out-of-office, mailbox full, delivery restrictions), session rate limiting, and recipient allowlist to prevent mistakes
  • ✏️ Draft emails for review — create, update, and send drafts; reply and forward as drafts; preview before saving with dry-run mode
  • 📅 Manage your calendar — view upcoming events, look back at past ones or find them by date range and subject, schedule meetings with attendees, update, decline or cancel events
  • 📦 Export emails — save individual messages to Markdown, EML, JSON, or CSV; export full conversation threads to MBOX or HTML; bulk-export search results in one call
  • 🔍 Investigate email headers — full raw header access (DKIM, SPF, DMARC, delivery chain, X-Mailer, X-Originating-IP) for phishing investigation and compliance review
  • 🗂️ Organise your inbox — create nested folders (addressable by path), set up inbox rules, colour-code with categories, manage Focused Inbox — all work together for complete inbox automation
  • 🔄 Track inbox changes — delta sync detects new, modified, and deleted emails since your last check, with tokens for incremental polling
  • 👥 Manage contacts — search your contact book and organisational directory, create and update contact records
  • ⚙️ Configure settings — set out-of-office auto-replies, working hours, and time zone
  • 📬 Access shared mailboxes — read and organise team inboxes and service accounts, including custom subfolders and nested folder paths; enumerate, read, and search the folder tree (best-effort — listings flag any branches skipped due to depth limits or per-folder errors), move/flag/categorise messages, and manage folders (work/school Microsoft 365 accounts; opt-in via OUTLOOK_SHARED_MAILBOX). Sending, drafts, replies, and forwards from a shared mailbox are not supported — those operations always act on the signed-in user's own mailbox
  • 🏢 Find meeting rooms — search by building, floor, capacity, AV equipment, and wheelchair accessibility (Microsoft 365)
Why Outlook Assistant?
Without Outlook Assistant With Outlook Assistant
Switch between your AI tool and Outlook to manage email Read, search, send, and export emails directly from your AI assistant
Manually search and export email threads Full email tools including search, threading, and bulk export
Context-switch for calendar and contacts Manage calendar events, contacts, and settings in one place
Copy-paste email content into conversations Your AI assistant reads your emails natively with full context
No programmatic access to mailbox rules or categories Create inbox rules, manage categories, configure auto-replies
Manually check each email for phishing red flags Forensic header analysis — DKIM, SPF, DMARC, spam scores, and delivery chain in one call
Poll your inbox to check for new mail Delta sync returns only changes since your last check, with tokens for continuous polling

Features

Module Tools What You Can Do
Email 8 search-emails (list/search/delta/conversations), read-email (content + forensic headers), send-email (with dry-run + mail tips), draft (create/update/send/delete/reply/reply-all/forward), update-email (read status, flags), attachments, export, get-mail-tips
Calendar 3 list-events (upcoming by default; startAfter/startBefore/subject filters), create-event, manage-event (update/decline/cancel/delete)
Contacts 2 manage-contact (list/search/get/create/update/delete), search-people
Categories 3 manage-category (CRUD), apply-category, manage-focused-inbox
Settings 1 mailbox-settings (get/set auto-replies/set working hours)
Folder 1 folders (list/create/move/stats/delete) — nested folders addressable by path (Parent/Child) or ID
Rules 1 manage-rules (list/create/update/reorder/delete)
Advanced 2 access-shared-mailbox (messages or folder tree), find-meeting-rooms
Auth 1 auth (status/authenticate/device-code-complete/about)

22 tools total — consolidated from 55 for optimal AI performance. See the Tools Reference for complete parameter details.

Export Formats

Format support varies by target:

Format Extension target=message (single) target=messages (batch) target=conversation (thread)
mime / eml .eml ✅ – ✅
mbox .mbox – – ✅
markdown .md ✅ ✅ ✅
json .json ✅ ✅ ✅
html .html – – ✅
csv .csv ✅ ✅ ✅

Export individual emails, search results, or entire conversation threads — use target=messages with a search query (or the query shortcut) to batch-export without manually collecting IDs.

Account Compatibility

Outlook Assistant works with both personal and work/school Microsoft accounts, but some features behave differently:

Feature Personal (Outlook.com) Work/School (Microsoft 365)
Email read, send, search Full support Full support
Calendar events Full support Full support
Contacts CRUD Full support Full support
Inbox rules Full support Full support
Folders Full support Full support
Free-text query search Limited — progressive fallback; subject, from, to filters are more direct Full $search support
Categories Full support Full support
Mailbox settings Full support Full support
Focused Inbox API works (overrides stored) but mail routing not affected Full support
Shared mailboxes Not available Opt-in (OUTLOOK_SHARED_MAILBOX). Read + organise only. Read: Mail.Read.Shared; organise (move/categorise/flag/create folders): Mail.ReadWrite.Shared. No sending/drafts/replies/forwards
Meeting room search Not available Requires Place.Read.All + admin consent

Note: On personal accounts, Microsoft's $search API has limited support for free-text queries. Outlook Assistant handles this automatically with progressive search — if your query returns no results, it falls back through OData filters, boolean filters, and recent message listing to find your emails. For the most direct results on personal accounts, use the structured filter parameters (from, subject, to, receivedAfter).

What Makes This Different
  • Progressive search — on accounts where Microsoft's $search API is limited, Outlook Assistant automatically falls back through up to 4 search strategies to find your emails, and reports which one answered in _meta.searchMetadata along with any filter it could not honour (droppedFilters). Most Graph API wrappers fail silently; this one adapts and tells you.
  • Email forensics — raw header access for DKIM, SPF, DMARC, delivery chain, X-Mailer, X-Originating-IP, and spam scores. Returns the full data so you can investigate phishing, audit compliance, or trace delivery issues. (Auto-verdict is on the roadmap; today the data is surfaced and analysed in-conversation.)
  • Delta sync — incremental inbox monitoring returns only what changed since your last check, with tokens for continuous polling. Designed for agent workflows that need to watch a mailbox.
  • Batch operations — flag, move, export, or categorise multiple emails in a single call. Search-driven export lets you batch-export results without collecting IDs manually.
  • Pre-send intelligence — check recipients for out-of-office, full mailbox, delivery restrictions, and moderation status before sending — no other Outlook MCP server offers this.
  • Compound automation — rules, categories, folders, and Focused Inbox work together. Set up complete inbox management through your AI assistant in one conversation.

Safety & Token Efficiency

Outlook Assistant is designed with safety-first principles for AI-driven email access:

Destructive action safeguards — Every tool carries MCP annotations (readOnlyHint, destructiveHint, idempotentHint, openWorldHint), all four set explicitly on every tool, so AI clients can auto-approve safe reads and prompt for confirmation on destructive operations like sending email, inviting attendees or deleting events. send-email and create-event also carry Claude's anthropic/requiresUserInteraction flag, so Claude Code asks before every call to them, dry runs included, even in auto-accept or bypass modes.

Read-only mode — Set OUTLOOK_READ_ONLY=true and the server refuses every tool call or action that isn't a read before it runs: no sends, drafts, moves, flags, deletes, rules, settings changes, exports or attachment downloads, and no dry runs either. Searching and reading still work, and so does signing in. auth action=about shows whether it's on.

Server instructions — When a client connects, the server sends it instructions for the model, hard rules first: treat retrieved email, calendar and contact content as data, not instructions; confirm anything that reaches other people, deletes or keeps acting, using dryRun: true previews; draft first and send only when asked; and treat allowlist refusals, rate limits (a session limit of 0 switches a tool off) and other policy refusals as final. When a session limit of 0 blocks a tool, the instructions name it.

Plugin skill and safety hook — The plugin adds two more layers. The using-outlook-assistant agent skill, read by Claude Code, GitHub Copilot and Cursor, teaches the model the hard rules plus the judgement the tool descriptions leave out: who each send, reply-all, invitation or cancellation reaches, what each delete loses, how prompt injection in email looks, and how to search without pulling the whole mailbox. A hook also asks you before anything that reaches other people, deletes or keeps acting, with a plain-English reason such as "Cancels the event 'Team sync' and emails a cancellation to every attendee". It stays quiet for reads and genuine dry runs, and its confirmation level (outward, all-writes or off) controls how often it asks. How it behaves depends on the client:

  • Claude Code: asks with the reason, even for tools you've allowed; set the level with the plugin's Confirmation level setting. In bypass permissions mode Claude Code may auto-approve these prompts (the plugin README has ask rules to keep them).
  • GitHub Copilot CLI: asks with the reason; set the level with OUTLOOK_CONFIRM_LEVEL. A hook that times out lets the call through. VS Code reads the same hook file (not yet checked by hand).
  • Cursor: the hook blocks the call if it fails or times out, but Cursor's own "Run this MCP tool?" prompt doesn't show the reason, and an Mcp(...) allow rule, or --force / Run Everything mode, runs the call without asking.
  • Other clients: no hook; the server's checks, annotations and instructions still apply.

See Supported Clients and Their Limits for the details.

Dry-run previews (dryRun: true) — See what a call would do without changing or sending anything: send-email, draft create, create-event (who would be invited, with a count of external addresses), manage-event update/decline/cancel/delete (who would be emailed), mailbox-settings set-auto-replies (who gets each reply, and when), manage-rules create/update, and folders delete and manage-contact delete (what would be lost). Any other call with dryRun: true is refused before it runs, so a preview can never send, delete or change anything for real.

Send-email protections — The send-email tool includes:

  • Pre-send mail tips (checkRecipients: true) — check recipients for out-of-office, mailbox full and delivery restrictions. If the tips show any of those, an external recipient or a group with external members, the send is refused with the warnings listed; repeat it with acknowledgeWarnings: true once you've seen them. A failed check also stops the send. Mail tips are Microsoft 365 only: personal accounts return none
  • Dry-run mode (dryRun: true) — preview composed emails without sending
  • Session rate limiting — configurable via OUTLOOK_MAX_EMAILS_PER_SESSION (default: no limit; 0 blocks sending and the other rate-limited tools)
  • Recipient allowlist — restrict recipients to approved addresses/domains via OUTLOOK_ALLOWED_RECIPIENTS. It covers send-email, draft (create, update, forward, reply, reply-all and send), rule forward/redirect (a rule that would forward or redirect to a blocked address is refused whole), create-event attendees and manage-event update attendees; it doesn't cover manage-event cancel/decline messages, the cancellation an organiser's delete sends, or mailbox-settings automatic replies. Anything that isn't a single plain email address is refused while it's set

Recommended setup: enable both safety belts in your .mcp.json from day one. They're off by default; auth action=about reports their state and prints a setup hint when unset. See .mcp.json.example for a copy-paste template.

"env": {
  "OUTLOOK_CLIENT_ID": "…",
  "OUTLOOK_MAX_EMAILS_PER_SESSION": "10",
  "OUTLOOK_ALLOWED_RECIPIENTS": "your-domain.com,[email protected]"
}

Input and file hardening — IDs containing . or .. path segments are refused before any request is made, continuation links (deltaToken) must point at graph.microsoft.com, and attachment downloads and exports write only inside the system temp directory, ~/Downloads, ~/Documents or OUTLOOK_EXPORT_DIR (never to dot-prefixed names), using sanitised filenames without overwriting existing files or following symlinks. Paths must be absolute (or start with ~/). An explicit export file path is replaced only when you pass overwrite: true, and never if it's a symlink. Files are created readable only by you (0600; new folders 0700).

Draft protections — The draft tool shares send-email safety controls: dry-run preview (create), mail-tips validation, rate limiting and the recipient allowlist. The allowlist is checked on create, update and forward; a reply or reply-all draft whose recipients it doesn't allow is deleted again; and send re-checks the draft's current to/cc/bcc, so a draft edited in Outlook can't slip past it. The send action shares the send-email rate limit counter, preventing circumvention via the draft-then-send pathway, so OUTLOOK_MAX_SEND_EMAIL_PER_SESSION=0 blocks both. A reply or reply-all draft that the allowlist refuses, or that couldn't be created, doesn't use up a draft session-limit slot. update, send and delete refuse any ID that is not an unsent draft, so a received or sent message is never edited, deleted or re-sent.

Token-optimised architecture — Tools are consolidated using the STRAP (Single Tool, Resource, Action Pattern) approach. 22 tools instead of 55 reduces per-turn overhead by ~11,000 tokens (~64%), keeping more of the AI's context window available for your actual conversation. Fewer tools also means the AI selects the right tool more accurately — research shows tool selection degrades beyond ~40 tools.

Important: These safeguards are defence-in-depth measures that reduce risk, but they are not a guarantee against unintended actions. AI-driven access to your email is inherently sensitive — always review tool calls before approving, particularly for sends and deletes. No automated guardrail is foolproof, and you remain responsible for actions taken through your mailbox.

Quick Start

1. Install
npm install -g @littlebearapps/outlook-assistant

Or run directly without installing:

npx @littlebearapps/outlook-assistant

To check which version you have, or to see the available options:

outlook-assistant --version     # prints e.g. 3.14.1
outlook-assistant --help        # usage, options and key environment variables

With no arguments the server speaks the Model Context Protocol over stdio. It's normally launched by your MCP client rather than run by hand — started from a terminal it will simply wait on stdin.

2. Register an Azure App

You need a Microsoft Azure app registration to authenticate. See the Azure Setup Guide for a detailed walkthrough (including first-time Azure account creation), or if you've done this before:

  1. Create a new app registration at portal.azure.com
  2. Add Microsoft Graph delegated permissions (Mail, Calendar, Contacts)
  3. (Browser flow only) Create a client secret and copy the Value (not the Secret ID). The default device-code sign-in doesn't need one
  4. Under Authentication > Add a platform > Mobile and desktop applications — check nativeclient URI
  5. Enable "Allow public client flows" in Authentication > Advanced settings
  6. (Optional) Set redirect URI to http://localhost:3333/auth/callback — only needed for browser auth flow
3. Configure Your MCP Client

Client support. Every MCP client gets the server's own checks. The plugin adds the using-outlook-assistant skill and a safety hook in Claude Code, GitHub Copilot and Cursor, with different limits in each. See Supported Clients and Their Limits.

Plugin install. The plugin (plugins/outlook-assistant) bundles the server pinned to an exact version, the skill and the safety hook. It follows both the Claude Code plugin format and the Agent Plugins format used by GitHub Copilot, plus a Cursor manifest (.cursor-plugin/).

  • Claude Code. The plugin asks for your settings when you enable it (client ID, sign-in audience, send limit per session, allowed recipients, read-only mode and confirmation level):

    claude plugin marketplace add littlebearapps/outlook-assistant
    claude plugin install outlook-assistant@littlebearapps
    
  • GitHub Copilot CLI. Copilot has no plugin settings, so give your client ID when you first sign in, and set the hook's confirmation level with the OUTLOOK_CONFIRM_LEVEL environment variable:

    copilot plugin marketplace add littlebearapps/outlook-assistant
    copilot plugin install outlook-assistant@littlebearapps
    

    VS Code's Copilot agent reads the same plugin and hook file; that hasn't been checked by hand yet.

  • Cursor (v3.14.0 or later). Cursor loads the folder as a Cursor plugin (.cursor-plugin/plugin.json). In Cursor CLI, load it from a clone of this repository with cursor-agent --plugin-dir outlook-assistant/plugins/outlook-assistant. Give your client ID when you first sign in. The v3.13.0 plugin can't sign in from Cursor (AADSTS900023); use the manual config below instead.

Manual config. Use this for Claude Desktop, Codex CLI, Gemini CLI, Windsurf and other MCP clients, or in place of a plugin (you then get no hook). Add to your MCP client config. Only OUTLOOK_CLIENT_ID is needed for the default device-code sign-in; add OUTLOOK_CLIENT_SECRET only if you use the browser flow. You can also leave the client ID out and give it to your assistant when you first connect (auth action=authenticate clientId=…), which saves it to ~/.outlook-assistant-config.json. An OUTLOOK_CLIENT_ID in the environment always takes precedence.

Claude Desktop (claude_desktop_config.json)
{
  "mcpServers": {
    "outlook": {
      "command": "npx",
      "args": ["@littlebearapps/outlook-assistant"],
      "env": {
        "OUTLOOK_CLIENT_ID": "your-application-client-id"
      }
    }
  }
}
Claude Code (CLI)
claude mcp add outlook \
  -e OUTLOOK_CLIENT_ID=your-application-client-id \
  -- npx -y @littlebearapps/outlook-assistant

The MCP server reads its settings from the environment your client passes it; it doesn't load a .env file.

VS Code / GitHub Copilot (.vscode/mcp.json)

VS Code prompts for the client ID the first time the server starts and stores it securely:

{
  "inputs": [
    {
      "type": "promptString",
      "id": "outlook-client-id",
      "description": "Azure application (client) ID"
    }
  ],
  "servers": {
    "outlook": {
      "type": "stdio",
      "command": "npx",
      "args": ["-y", "@littlebearapps/outlook-assistant"],
      "env": {
        "OUTLOOK_CLIENT_ID": "${input:outlook-client-id}"
      }
    }
  }
}

Use it from Copilot Chat in Agent mode. To use it in every workspace, add the same entry to your user mcp.json (Command Palette → MCP: Open User Configuration).

Cursor (.cursor/mcp.json)

Install in Cursor

Or add manually to .cursor/mcp.json:

{
  "mcpServers": {
    "outlook": {
      "command": "npx",
      "args": ["@littlebearapps/outlook-assistant"],
      "env": {
        "OUTLOOK_CLIENT_ID": "your-application-client-id"
      }
    }
  }
}
Windsurf (~/.codeium/windsurf/mcp_config.json)
{
  "mcpServers": {
    "outlook": {
      "command": "npx",
      "args": ["@littlebearapps/outlook-assistant"],
      "env": {
        "OUTLOOK_CLIENT_ID": "your-application-client-id"
      }
    }
  }
}
4. Authenticate
  1. Ask your AI assistant to connect to Outlook — it calls the auth tool with action=authenticate and returns a short code and the URL microsoft.com/devicelogin
  2. Open the URL on any device (a private/incognito window avoids cached sessions), enter the code, sign in and grant permissions
  3. Tell your assistant you're done — it calls auth with action=device-code-complete
  4. Tokens are saved locally and refresh automatically

No auth server is needed for this default device-code flow. If you'd rather use the browser redirect flow, see Authentication Flow below.

Installation

Prerequisites
  • Node.js 18.18.0 or higher (contributors: the dev tooling needs 22.22.1 or higher)
  • npm (included with Node.js)
  • Azure account for app registration (free tier works)
npm install -g @littlebearapps/outlook-assistant
From source
git clone https://github.com/littlebearapps/outlook-assistant.git
cd outlook-assistant
npm install
CLI options
Option What it does
-v, --version Print the version to stdout and exit 0
-h, --help Print usage, options and key environment variables, and exit 0
(none) Start the MCP server on stdio — the normal mode, invoked by your MCP client

An unrecognised argument is reported on stderr and exits 1, rather than starting a server that would ignore it.

Azure App Registration

First time with Azure? The Azure Setup Guide covers everything from creating an account to your first authentication, including billing setup and common pitfalls.

Create the App
  1. Open Azure Portal
  2. Sign in with a Microsoft Work or Personal account
  3. Search for App registrations and click New registration
  4. Enter a name (e.g. "Outlook Assistant Server")
  5. Select Accounts in any organizational directory and personal Microsoft accounts
  6. Set redirect URI: platform Web, URI http://localhost:3333/auth/callback
  7. Click Register
  8. Copy the Application (client) ID
Add Permissions
  1. Go to API permissions > Add a permission > Microsoft Graph > Delegated permissions
  2. Add these required permissions:
    • offline_access — refresh tokens between sessions
    • User.Read — basic profile
    • Mail.Read, Mail.ReadWrite, Mail.Send — email operations
    • Calendars.Read, Calendars.ReadWrite — calendar operations
    • Contacts.Read, Contacts.ReadWrite — contact management
    • MailboxSettings.ReadWrite — settings, auto-replies, categories
    • People.Read — people search
  3. Optionally add org-only permissions (work/school accounts only):
    • Mail.Read.Shared — shared mailbox read access (requested only when OUTLOOK_SHARED_MAILBOX=read or =true)
    • Mail.ReadWrite.Shared — shared mailbox writes (move/categorise/flag/mark-read; requested only when OUTLOOK_SHARED_MAILBOX=true)
    • Place.Read.All — meeting room search (requires admin consent)
  4. Click Add permissions
Create a Client Secret

Only needed for the browser redirect flow. Skip this if you sign in with the default device code.

  1. Go to Certificates & secrets > New client secret
  2. Enter a description and select expiration
  3. Click Add
  4. Copy the secret Value immediately — you won't be able to see it again. Use the Value, not the Secret ID.

Configuration

Environment Variables

Set these in your MCP client's "env" block (see Quick Start). The MCP server doesn't load .env files; the browser-flow auth server (npm run auth-server) does, so when running from source you can also keep a .env for it:

cp .env.example .env

Edit with your Azure credentials:

OUTLOOK_CLIENT_ID=your-application-client-id
OUTLOOK_CLIENT_SECRET=your-client-secret-VALUE
USE_TEST_MODE=false

Note: The server also accepts MS_CLIENT_ID and MS_CLIENT_SECRET for backwards compatibility.

Optional overrides (v3.8.0+) — see .env.example for the full list with commented worked examples:

Variable Purpose Default
OUTLOOK_AUTH_AUDIENCE OAuth audience: common, consumers (personal-only Azure apps), organizations, or single-tenant GUID. Fixes AADSTS9002331 for personal-only app registrations. common
OUTLOOK_DEFAULT_TIMEZONE IANA timezone applied to calendar events when callers don't pass one (e.g. Europe/London, America/New_York). Australia/Melbourne
OUTLOOK_MAX_EMAILS_PER_SESSION Default per-session cap for each rate-limited tool, counted separately until the server restarts: send-email (including draft action=send), draft create/update/reply/reply-all/forward, manage-rules and create-event. Override one tool with OUTLOOK_MAX_<TOOL>_PER_SESSION, e.g. OUTLOOK_MAX_SEND_EMAIL_PER_SESSION. Unset or empty means no limit; 0 blocks the tool (before v3.14.1, 0 meant no limit), and so does any value that isn't a whole number. no limit
OUTLOOK_ALLOWED_RECIPIENTS Comma-separated allowlist of domains/addresses for sends, drafts, rule forwards and calendar invitations (create-event and manage-event update attendees). Not applied to cancellation/decline messages or automatic replies. unrestricted
OUTLOOK_SHARED_MAILBOX Opt-in shared-mailbox support (work/school only). read requests Mail.Read.Shared; true (or readwrite/1) also requests Mail.ReadWrite.Shared. Unset leaves sign-in unchanged. After enabling, restart and run auth action=authenticate force=true. unset (off)
OUTLOOK_SEARCH_SCAN_LIMIT How many recent messages the client-side search fallback scans. Personal accounts match to locally within this window, so the default caps how far back a to search reaches. Max 5000. 500
OUTLOOK_REQUEST_TIMEOUT_MS Inactivity timeout for each Graph request attempt, in milliseconds: an attempt that receives no data for this long is abandoned with a timeout error. It isn't an overall deadline, so a slow response that keeps arriving isn't cut off. Throttled (429) and busy (503/504) responses are retried automatically, honouring Retry-After. 60000
OUTLOOK_READ_ONLY Read-only mode: true (or 1/yes/on) refuses every tool call or action that isn't a read, including dry runs, exports and attachment downloads, before it runs. Signing in still works. An unrecognised value also turns it on, with a warning. Restart the server after changing it. off
OUTLOOK_DEBUG Detailed stderr logs: true (or 1/yes/on) adds search strategies, subjects, folder names and Graph error bodies, with email addresses and long IDs redacted. Off, each tool call logs one line (tool, action, outcome, duration) and never its arguments. Tokens, device codes and secrets are never logged. See Server Logs and Debug Logging. off
OUTLOOK_EXPORT_DIR Extra folder that export and attachments downloads may write into. Without it, files can only go to the system temp directory, ~/Downloads or ~/Documents; other paths are refused. Absolute path (a leading ~ is expanded). unset

OUTLOOK_CONFIRM_LEVEL (outward, all-writes or off; default outward) isn't a server setting: the plugin's safety hook reads it, in clients with no plugin settings (GitHub Copilot, VS Code, Cursor). Set it in the environment the client starts from, not in the server's env block. In Claude Code, use the plugin's Confirmation level setting instead. See Supported Clients and Their Limits.

MCP Client Configuration

See Quick Start — Configure Your MCP Client above for the plugin installs and the Claude Desktop, Claude Code, VS Code / GitHub Copilot, Cursor, and Windsurf configs.

If installed from source, use node instead of npx:

{
  "mcpServers": {
    "outlook": {
      "command": "node",
      "args": ["/path/to/outlook-assistant/index.js"],
      "env": {
        "OUTLOOK_CLIENT_ID": "your-application-client-id"
      }
    }
  }
}

Authentication Flow

No auth server needed. Works everywhere, including remote/headless environments.

  1. Ask your AI assistant to authenticate (calls auth tool with action=authenticate)
  2. Visit the URL shown (microsoft.com/devicelogin) on any browser, any device
  3. Enter the code, sign in with your Microsoft account, and grant permissions
  4. Tell your AI assistant to complete authentication (calls auth with action=device-code-complete)
  5. Tokens are saved to ~/.outlook-assistant-tokens.json and refresh automatically

Prerequisite: Enable "Allow public client flows" in Azure Portal > your app > Authentication > Advanced settings.

Server restarts (v3.7.2+): Device code state is persisted to ~/.outlook-assistant-pending-auth.json, so device-code-complete works even if the MCP server restarts between steps 1 and 4 (e.g., Untether/Telegram bridge, Claude Desktop session changes).

Browser Redirect Flow (Alternative)

For localhost development or if you prefer the traditional OAuth flow, start the auth server. From a source checkout:

npm run auth-server

From a global npm install:

node "$(npm root -g)/@littlebearapps/outlook-assistant/outlook-auth-server.js"

This starts a local server on port 3333 to handle the OAuth callback. (The outlook-assistant command itself only accepts --version and --help; any other argument exits with an error.)

  1. In your AI assistant, use the auth tool with action=authenticate, method=browser
  2. Open the provided URL in your browser
  3. Sign in and grant permissions — tokens are saved automatically

Note: The auth server reads OUTLOOK_CLIENT_ID and OUTLOOK_CLIENT_SECRET from environment variables or a .env file in the directory you start it from. Your MCP client's "env" config only applies to the MCP server process, not a separately-started auth server.

Shared mailboxes: the browser flow requests the configured scopes with no fallback. If you enable OUTLOOK_SHARED_MAILBOX, sign in with the device-code flow.

Directory Structure

outlook-assistant/
├── index.js                 # Entry point: CLI flags, stdio transport
├── server.js                # MCP server factory (capabilities, request handler)
├── tools.js                 # Tool registry (22 tools)
├── request-handler.js       # Routes MCP requests; JSON-RPC errors for unknown methods/tools
├── config.js                # Configuration settings
├── outlook-auth-server.js   # OAuth server (port 3333)
├── auth/                    # Authentication module (1 tool)
├── email/                   # Email module (8 tools)
│   ├── mail-tips.js         # Pre-send recipient validation
│   ├── headers.js           # Email header retrieval
│   ├── mime.js              # Raw MIME/EML content
│   ├── conversations.js     # Thread listing/export
│   ├── attachments.js       # Attachment operations
│   └── ...
├── calendar/                # Calendar module (3 tools)
│   ├── attendees.js         # Attendee builder (email or {email, type})
│   └── list.js              # list-events filters
├── contacts/                # Contacts module (2 tools)
├── categories/              # Categories module (3 tools)
├── settings/                # Settings module (1 tool)
├── folder/                  # Folder module (1 tool; resolve.js resolves paths/IDs)
├── rules/                   # Rules module (1 tool)
├── advanced/                # Advanced module (2 tools)
└── utils/
    ├── graph-api.js         # Microsoft Graph API client (includes $batch, path guards)
    ├── mailbox.js           # me vs users/{sharedMailbox} prefix, shared-mailbox opt-in
    ├── risk-classes.js      # Risk class per tool/action; derives annotations and titles
    ├── tool-error.js        # isError tool results with a next step
    ├── safety.js            # Rate limiting, recipient allowlist, dry-run
    ├── safe-write.js        # Exclusive, folder-confined file writes
    ├── datetime.js          # ISO 8601 parsing and timezone conversion
    ├── odata-helpers.js     # OData query building
    ├── field-presets.js     # Token-efficient field selections
    ├── response-formatter.js # Verbosity levels
    └── mock-data.js         # Test mode data

Troubleshooting

"Cannot find module '@modelcontextprotocol/sdk/server/index.js'"
npm install
"EADDRINUSE: address already in use :::3333"
npx kill-port 3333
npm run auth-server
"Invalid client secret" (AADSTS7000215)

You're using the Secret ID instead of the Secret Value. Go to Azure Portal > Certificates & secrets and copy the Value column into OUTLOOK_CLIENT_SECRET.

The Value is shown only once, when the secret is created — if you've navigated away it can't be read again, so create a new secret. An expired secret produces this same error, so check the Expires column too.

Since v3.11.0 the server detects this error and appends the explanation to Microsoft's original message, so you see both the raw error code and what to do about it.

Authentication URL doesn't work

If using browser flow: start the auth server first with npm run auth-server. If using device code flow: visit microsoft.com/devicelogin instead.

Device code "invalid_client"

Enable "Allow public client flows" in Azure Portal > App registrations > Authentication > Advanced settings.

Token refresh fails after ~60 minutes (device code auth)

Fixed in v3.7.2. Earlier versions sent client_secret in token refresh requests for device-code auth, which Microsoft rejects for public client flows. Update to v3.7.2+ or re-authenticate.

"Authentication required."

You're signed out, or the saved token expired and couldn't be refreshed. The error says what to do next: sign in with the auth tool with action=authenticate (add force=true to replace an existing session), then retry the call. auth action=status shows the current state.

Development

Running Tests
npm test                     # Jest unit tests
npm run inspect              # MCP Inspector (interactive)
Test Mode

Run with mock data (no real API calls):

USE_TEST_MODE=true npm start
Extending the Server
  1. Create a new module directory (e.g. tasks/)
  2. Implement tool handlers in separate files
  3. Export tool definitions from the module's index.js
  4. Add the module's tools to the TOOLS array in tools.js
  5. Classify every tool and action in utils/risk-classes.js (a test fails on anything unclassified); the annotations and title come from there
  6. Add tests in test/
  7. Update docs/quickrefs/tools-reference.md

Documentation

Guide Description
Getting Started Install, configure, and authenticate — start here
Supported Clients Install per client, what the skill and safety hook do in each, and known limits
Azure Setup Guide Azure account creation, app registration, permissions, and secrets
How-To Guides 30 practical guides for email, calendar, contacts, and settings
Roadmap Active milestones (v3.14.1, v3.15.0, v4.0.0, v3.8.x, v3.16.0+) and recent releases
Troubleshooting Known errors and fixes, including auth, search, export and shared mailboxes
FAQ Install, accounts, permissions, tokens, updates, uninstall
Tools Reference All 22 tools with parameters
AI Agent Guide Tool selection and workflow patterns for AI agents

Full documentation: docs/

Known Limitations

  • Personal account search: Free-text query and the raw searchExpression (formerly kqlQuery) rely on Microsoft's $search API, which has limited support on personal Outlook.com accounts. query mitigates this with progressive fallback (OData filters, boolean filters, then a client-side scan). Field-scoped $search (e.g. subject:"…") is rejected outright there; since v3.10.0 from:/to:/subject: expressions are translated into the closest equivalent OData filters and retried, but boolean operators, grouping, wildcards and other field prefixes are not — those still terminate with an explicit no-results rather than a silent broader search. Structured filters (from, subject, to, receivedAfter) remain the most direct route. Cross-folder search (searchAllFolders: true) returns a superset of inbox-only results. Note that query and searchExpression are not interchangeable there: searchExpression goes to $search, which matches the whole message including the body and ranks by relevance rather than date, while query falls back to a subject substring match that never reads bodies.
  • to search depth on personal accounts: the server-side recipient filter is rejected, so to is matched locally over the 500 most recent messages (OUTLOOK_SEARCH_SCAN_LIMIT, max 5000). On a large archive that excludes older mail — pair to with receivedAfter/receivedBefore. Since v3.11.1 the response says so whenever the scan was truncated, whether or not it matched.
  • Focused Inbox: Only available on work/school Microsoft 365 accounts.
  • Shared mailboxes: Require a work/school account and are opt-in: set OUTLOOK_SHARED_MAILBOX=read (read) or =true (read and organise), restart the server, then re-authenticate with auth action=authenticate force=true. Until then, sharedMailbox calls are refused with setup guidance (access-shared-mailbox keeps its previous well-known-folder behaviour). auth action=about shows whether the shared scopes were actually granted. Support covers reading and organising only. Reading needs Mail.Read.Shared; organising (move/categorise/flag/mark-read/create folders via sharedMailbox) needs Mail.ReadWrite.Shared — add it in Azure and re-authenticate (until then, shared-scoped writes fail with 403; they never fall back to your own mailbox). Custom subfolders are supported — pass folder as a display name or nested path (e.g. Inbox/Vendors/Acme), a raw folderId, or use listFolders: true (or folders action=list, sharedMailbox: …) to discover them. Sending, drafts, replies, and forwards from a shared mailbox are not supported — send-email and draft (including reply/reply-all/forward) always act on the signed-in user's own mailbox, and Mail.Send.Shared is not requested.
  • Meeting room search: Requires Place.Read.All permission with admin consent (work/school accounts only).
  • Export default path: Exports and attachment downloads save to the system temp directory by default (a batch export with target=messages needs an outputDir). Use outputDir (or savePath) with an absolute path (or one starting with ~/) inside the system temp directory, ~/Downloads, ~/Documents or OUTLOOK_EXPORT_DIR; relative paths and other folders are refused. An existing savePath file is replaced only with overwrite: true.
  • list-events date filters: startAfter/startBefore must include Z or a ±hh:mm offset; zone-less and date-only values are rejected rather than guessed.

Contributing

Contributions are welcome! Please see CONTRIBUTING.md for guidelines.

Security

For security concerns, please see our Security Policy. Do not open public issues for vulnerabilities.

Changelog

See CHANGELOG.md for version history.

About

Built and maintained by Little Bear Apps. Outlook Assistant is open source under the MIT License.

1<p align="center">
2 <img src="https://raw.githubusercontent.com/littlebearapps/outlook-assistant/main/docs/assets/outlook-assistant-logo-full.png" height="200" alt="Outlook Assistant" />
3</p>
4 
5<h1 align="center">Outlook Assistant</h1>
6 
7<p align="center">
8 <strong>MCP server for Outlook email, calendar, and contacts — let your AI assistant manage your inbox directly from the conversation.</strong>
9</p>
10 
11<p align="center">
12 <a href="https://www.npmjs.com/package/@littlebearapps/outlook-assistant"><img src="https://img.shields.io/npm/v/@littlebearapps/outlook-assistant" alt="npm version" /></a>
13 <a href="https://www.npmjs.com/package/@littlebearapps/outlook-assistant"><img src="https://img.shields.io/npm/dm/@littlebearapps/outlook-assistant" alt="npm downloads" /></a>
14 <a href="https://github.com/littlebearapps/outlook-assistant/actions/workflows/ci.yml"><img src="https://github.com/littlebearapps/outlook-assistant/actions/workflows/ci.yml/badge.svg" alt="CI" /></a>
15 <a href="https://github.com/littlebearapps/outlook-assistant/actions/workflows/codeql.yml"><img src="https://github.com/littlebearapps/outlook-assistant/actions/workflows/codeql.yml/badge.svg" alt="CodeQL" /></a>
16 <a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT" /></a>
17 <a href="https://glama.ai/mcp/servers/littlebearapps/outlook-assistant"><img src="https://glama.ai/mcp/servers/littlebearapps/outlook-assistant/badges/score.svg" alt="Glama score" /></a>
18</p>
19 
20Outlook Assistant connects AI assistants to your Microsoft Outlook account through the [Model Context Protocol](https://modelcontextprotocol.io/). Ask your AI assistant to search your inbox, send emails, schedule meetings, manage contacts, and configure mailbox settings — without leaving the conversation. Works with Claude, GitHub Copilot, Cursor, Windsurf, and any MCP-compatible client.
21 
22**Works with personal Outlook.com and work/school Microsoft 365 accounts.**
23 
24<div align="center">
25 <br />
26 <a href="https://github.com/littlebearapps/outlook-assistant/blob/main/docs/demo/outlook-assistant-demo.mp4">
27 <img src="https://raw.githubusercontent.com/littlebearapps/outlook-assistant/main/docs/demo/outlook-assistant-demo.gif" alt="Outlook Assistant Demo — searching emails, reading, and drafting a reply" width="720" style="border-radius: 12px; box-shadow: 0 8px 32px rgba(0,0,0,0.12);" />
28 </a>
29 <br />
30 <sub>Search inbox → read &amp; summarise → draft a reply — all from the conversation</sub>
31 <br /><br />
32</div>
33 
34### What you can do
35 
36- 📨 **Search and read emails** — find messages by sender, subject, date, or keywords; read full threads with conversation grouping; batch flag, move, export, or categorise multiple emails at once
37- 🛡️ **Send emails with safety controls** — dry-run preview, pre-send mail tips (out-of-office, mailbox full, delivery restrictions), session rate limiting, and recipient allowlist to prevent mistakes
38- ✏️ **Draft emails for review** — create, update, and send drafts; reply and forward as drafts; preview before saving with dry-run mode
39- 📅 **Manage your calendar** — view upcoming events, look back at past ones or find them by date range and subject, schedule meetings with attendees, update, decline or cancel events
40- 📦 **Export emails** — save individual messages to Markdown, EML, JSON, or CSV; export full conversation threads to MBOX or HTML; bulk-export search results in one call
41- 🔍 **Investigate email headers** — full raw header access (DKIM, SPF, DMARC, delivery chain, X-Mailer, X-Originating-IP) for phishing investigation and compliance review
42- 🗂️ **Organise your inbox** — create nested folders (addressable by path), set up inbox rules, colour-code with categories, manage Focused Inbox — all work together for complete inbox automation
43- 🔄 **Track inbox changes** — delta sync detects new, modified, and deleted emails since your last check, with tokens for incremental polling
44- 👥 **Manage contacts** — search your contact book and organisational directory, create and update contact records
45- ⚙️ **Configure settings** — set out-of-office auto-replies, working hours, and time zone
46- 📬 **Access shared mailboxes** — read and organise team inboxes and service accounts, including custom subfolders and nested folder paths; enumerate, read, and search the folder tree (best-effort — listings flag any branches skipped due to depth limits or per-folder errors), move/flag/categorise messages, and manage folders (work/school Microsoft 365 accounts; opt-in via `OUTLOOK_SHARED_MAILBOX`). Sending, drafts, replies, and forwards from a shared mailbox are not supported — those operations always act on the signed-in user's own mailbox
47- 🏢 **Find meeting rooms** — search by building, floor, capacity, AV equipment, and wheelchair accessibility (Microsoft 365)
48 
49### Why Outlook Assistant?
50 
51| Without Outlook Assistant | With Outlook Assistant |
52|---------------------|------------------|
53| Switch between your AI tool and Outlook to manage email | Read, search, send, and export emails directly from your AI assistant |
54| Manually search and export email threads | Full email tools including search, threading, and bulk export |
55| Context-switch for calendar and contacts | Manage calendar events, contacts, and settings in one place |
56| Copy-paste email content into conversations | Your AI assistant reads your emails natively with full context |
57| No programmatic access to mailbox rules or categories | Create inbox rules, manage categories, configure auto-replies |
58| Manually check each email for phishing red flags | Forensic header analysis — DKIM, SPF, DMARC, spam scores, and delivery chain in one call |
59| Poll your inbox to check for new mail | Delta sync returns only changes since your last check, with tokens for continuous polling |
60 
61## Features
62 
63| Module | Tools | What You Can Do |
64|--------|------:|-----------------|
65| **Email** | 8 | `search-emails` (list/search/delta/conversations), `read-email` (content + forensic headers), `send-email` (with dry-run + mail tips), `draft` (create/update/send/delete/reply/reply-all/forward), `update-email` (read status, flags), `attachments`, `export`, `get-mail-tips` |
66| **Calendar** | 3 | `list-events` (upcoming by default; `startAfter`/`startBefore`/`subject` filters), `create-event`, `manage-event` (update/decline/cancel/delete) |
67| **Contacts** | 2 | `manage-contact` (list/search/get/create/update/delete), `search-people` |
68| **Categories** | 3 | `manage-category` (CRUD), `apply-category`, `manage-focused-inbox` |
69| **Settings** | 1 | `mailbox-settings` (get/set auto-replies/set working hours) |
70| **Folder** | 1 | `folders` (list/create/move/stats/delete) — nested folders addressable by path (`Parent/Child`) or ID |
71| **Rules** | 1 | `manage-rules` (list/create/update/reorder/delete) |
72| **Advanced** | 2 | `access-shared-mailbox` (messages or folder tree), `find-meeting-rooms` |
73| **Auth** | 1 | `auth` (status/authenticate/device-code-complete/about) |
74 
75**22 tools total** — consolidated from 55 for optimal AI performance. See the [Tools Reference](docs/quickrefs/tools-reference.md) for complete parameter details.
76 
77### Export Formats
78 
79Format support varies by `target`:
80 
81| Format | Extension | `target=message` (single) | `target=messages` (batch) | `target=conversation` (thread) |
82|--------|-----------|--------|--------|--------|
83| `mime` / `eml` | `.eml` | ✅ | – | ✅ |
84| `mbox` | `.mbox` | – | – | ✅ |
85| `markdown` | `.md` | ✅ | ✅ | ✅ |
86| `json` | `.json` | ✅ | ✅ | ✅ |
87| `html` | `.html` | – | – | ✅ |
88| `csv` | `.csv` | ✅ | ✅ | ✅ |
89 
90Export individual emails, search results, or entire conversation threads — use `target=messages` with a search query (or the `query` shortcut) to batch-export without manually collecting IDs.
91 
92## Account Compatibility
93 
94Outlook Assistant works with both personal and work/school Microsoft accounts, but some features behave differently:
95 
96| Feature | Personal (Outlook.com) | Work/School (Microsoft 365) |
97|---------|----------------------|---------------------------|
98| Email read, send, search | Full support | Full support |
99| Calendar events | Full support | Full support |
100| Contacts CRUD | Full support | Full support |
101| Inbox rules | Full support | Full support |
102| Folders | Full support | Full support |
103| Free-text `query` search | Limited — progressive fallback; `subject`, `from`, `to` filters are more direct | Full `$search` support |
104| Categories | Full support | Full support |
105| Mailbox settings | Full support | Full support |
106| Focused Inbox | API works (overrides stored) but mail routing not affected | Full support |
107| Shared mailboxes | Not available | Opt-in (`OUTLOOK_SHARED_MAILBOX`). Read + organise only. Read: `Mail.Read.Shared`; organise (move/categorise/flag/create folders): `Mail.ReadWrite.Shared`. No sending/drafts/replies/forwards |
108| Meeting room search | Not available | Requires `Place.Read.All` + admin consent |
109 
110> **Note**: On personal accounts, Microsoft's `$search` API has limited support for free-text queries. Outlook Assistant handles this automatically with progressive search — if your query returns no results, it falls back through OData filters, boolean filters, and recent message listing to find your emails. For the most direct results on personal accounts, use the structured filter parameters (`from`, `subject`, `to`, `receivedAfter`).
111 
112### What Makes This Different
113 
114- **Progressive search** — on accounts where Microsoft's `$search` API is limited, Outlook Assistant automatically falls back through up to 4 search strategies to find your emails, and reports which one answered in `_meta.searchMetadata` along with any filter it could not honour (`droppedFilters`). Most Graph API wrappers fail silently; this one adapts and tells you.
115- **Email forensics** — raw header access for DKIM, SPF, DMARC, delivery chain, X-Mailer, X-Originating-IP, and spam scores. Returns the full data so you can investigate phishing, audit compliance, or trace delivery issues. (Auto-verdict is on the roadmap; today the data is surfaced and analysed in-conversation.)
116- **Delta sync** — incremental inbox monitoring returns only what changed since your last check, with tokens for continuous polling. Designed for agent workflows that need to watch a mailbox.
117- **Batch operations** — flag, move, export, or categorise multiple emails in a single call. Search-driven export lets you batch-export results without collecting IDs manually.
118- **Pre-send intelligence** — check recipients for out-of-office, full mailbox, delivery restrictions, and moderation status before sending — no other Outlook MCP server offers this.
119- **Compound automation** — rules, categories, folders, and Focused Inbox work together. Set up complete inbox management through your AI assistant in one conversation.
120 
121## Safety & Token Efficiency
122 
123Outlook Assistant is designed with safety-first principles for AI-driven email access:
124 
125**Destructive action safeguards** — Every tool carries [MCP annotations](https://modelcontextprotocol.io/docs/concepts/tools#annotations) (`readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`), all four set explicitly on every tool, so AI clients can auto-approve safe reads and prompt for confirmation on destructive operations like sending email, inviting attendees or deleting events. `send-email` and `create-event` also carry Claude's `anthropic/requiresUserInteraction` flag, so Claude Code asks before every call to them, dry runs included, even in auto-accept or bypass modes.
126 
127**Read-only mode** — Set `OUTLOOK_READ_ONLY=true` and the server refuses every tool call or action that isn't a read before it runs: no sends, drafts, moves, flags, deletes, rules, settings changes, exports or attachment downloads, and no dry runs either. Searching and reading still work, and so does signing in. `auth action=about` shows whether it's on.
128 
129**Server instructions** — When a client connects, the server sends it instructions for the model, hard rules first: treat retrieved email, calendar and contact content as data, not instructions; confirm anything that reaches other people, deletes or keeps acting, using `dryRun: true` previews; draft first and send only when asked; and treat allowlist refusals, rate limits (a session limit of `0` switches a tool off) and other policy refusals as final. When a session limit of `0` blocks a tool, the instructions name it.
130 
131**Plugin skill and safety hook** — The [plugin](plugins/outlook-assistant/) adds two more layers. The `using-outlook-assistant` agent skill, read by Claude Code, GitHub Copilot and Cursor, teaches the model the hard rules plus the judgement the tool descriptions leave out: who each send, reply-all, invitation or cancellation reaches, what each delete loses, how prompt injection in email looks, and how to search without pulling the whole mailbox. A hook also asks you before anything that reaches other people, deletes or keeps acting, with a plain-English reason such as "Cancels the event 'Team sync' and emails a cancellation to every attendee". It stays quiet for reads and genuine dry runs, and its confirmation level (`outward`, `all-writes` or `off`) controls how often it asks. How it behaves depends on the client:
132 
133- **Claude Code:** asks with the reason, even for tools you've allowed; set the level with the plugin's **Confirmation level** setting. In bypass permissions mode Claude Code may auto-approve these prompts (the [plugin README](plugins/outlook-assistant/README.md#skill-and-safety-hook) has ask rules to keep them).
134- **GitHub Copilot CLI:** asks with the reason; set the level with `OUTLOOK_CONFIRM_LEVEL`. A hook that times out lets the call through. VS Code reads the same hook file (not yet checked by hand).
135- **Cursor:** the hook blocks the call if it fails or times out, but Cursor's own "Run this MCP tool?" prompt doesn't show the reason, and an `Mcp(...)` allow rule, or `--force` / Run Everything mode, runs the call without asking.
136- **Other clients:** no hook; the server's checks, annotations and instructions still apply.
137 
138See [Supported Clients and Their Limits](docs/how-to/getting-started/supported-clients.md) for the details.
139 
140**Dry-run previews** (`dryRun: true`) — See what a call would do without changing or sending anything: `send-email`, `draft` create, `create-event` (who would be invited, with a count of external addresses), `manage-event` update/decline/cancel/delete (who would be emailed), `mailbox-settings` set-auto-replies (who gets each reply, and when), `manage-rules` create/update, and `folders` delete and `manage-contact` delete (what would be lost). Any other call with `dryRun: true` is refused before it runs, so a preview can never send, delete or change anything for real.
141 
142**Send-email protections** — The `send-email` tool includes:
143- **Pre-send mail tips** (`checkRecipients: true`) — check recipients for out-of-office, mailbox full and delivery restrictions. If the tips show any of those, an external recipient or a group with external members, the send is refused with the warnings listed; repeat it with `acknowledgeWarnings: true` once you've seen them. A failed check also stops the send. Mail tips are Microsoft 365 only: personal accounts return none
144- **Dry-run mode** (`dryRun: true`) — preview composed emails without sending
145- **Session rate limiting** — configurable via `OUTLOOK_MAX_EMAILS_PER_SESSION` (default: no limit; `0` blocks sending and the other rate-limited tools)
146- **Recipient allowlist** — restrict recipients to approved addresses/domains via `OUTLOOK_ALLOWED_RECIPIENTS`. It covers `send-email`, `draft` (create, update, forward, reply, reply-all and send), rule forward/redirect (a rule that would forward or redirect to a blocked address is refused whole), `create-event` attendees and `manage-event` update attendees; it doesn't cover `manage-event` cancel/decline messages, the cancellation an organiser's delete sends, or `mailbox-settings` automatic replies. Anything that isn't a single plain email address is refused while it's set
147 
148> **Recommended setup**: enable both safety belts in your `.mcp.json` from day one. They're off by default; `auth action=about` reports their state and prints a setup hint when unset. See [`.mcp.json.example`](.mcp.json.example) for a copy-paste template.
149>
150> ```json
151> "env": {
152> "OUTLOOK_CLIENT_ID": "…",
153> "OUTLOOK_MAX_EMAILS_PER_SESSION": "10",
154> "OUTLOOK_ALLOWED_RECIPIENTS": "your-domain.com,[email protected]"
155> }
156> ```
157 
158**Input and file hardening** — IDs containing `.` or `..` path segments are refused before any request is made, continuation links (`deltaToken`) must point at `graph.microsoft.com`, and attachment downloads and exports write only inside the system temp directory, `~/Downloads`, `~/Documents` or `OUTLOOK_EXPORT_DIR` (never to dot-prefixed names), using sanitised filenames without overwriting existing files or following symlinks. Paths must be absolute (or start with `~/`). An explicit `export` file path is replaced only when you pass `overwrite: true`, and never if it's a symlink. Files are created readable only by you (`0600`; new folders `0700`).
159 
160**Draft protections** — The `draft` tool shares `send-email` safety controls: dry-run preview (`create`), mail-tips validation, rate limiting and the recipient allowlist. The allowlist is checked on create, update and forward; a reply or reply-all draft whose recipients it doesn't allow is deleted again; and `send` re-checks the draft's current to/cc/bcc, so a draft edited in Outlook can't slip past it. The `send` action shares the `send-email` rate limit counter, preventing circumvention via the draft-then-send pathway, so `OUTLOOK_MAX_SEND_EMAIL_PER_SESSION=0` blocks both. A reply or reply-all draft that the allowlist refuses, or that couldn't be created, doesn't use up a `draft` session-limit slot. `update`, `send` and `delete` refuse any ID that is not an unsent draft, so a received or sent message is never edited, deleted or re-sent.
161 
162**Token-optimised architecture** — Tools are consolidated using the STRAP (Single Tool, Resource, Action Pattern) approach. 22 tools instead of 55 reduces per-turn overhead by ~11,000 tokens (~64%), keeping more of the AI's context window available for your actual conversation. Fewer tools also means the AI selects the right tool more accurately — research shows tool selection degrades beyond ~40 tools.
163 
164> **Important**: These safeguards are defence-in-depth measures that reduce risk, but they are not a guarantee against unintended actions. AI-driven access to your email is inherently sensitive — always review tool calls before approving, particularly for sends and deletes. No automated guardrail is foolproof, and you remain responsible for actions taken through your mailbox.
165 
166## Quick Start
167 
168### 1. Install
169 
170```bash
171npm install -g @littlebearapps/outlook-assistant
172```
173 
174Or run directly without installing:
175 
176```bash
177npx @littlebearapps/outlook-assistant
178```
179 
180To check which version you have, or to see the available options:
181 
182```bash
183outlook-assistant --version # prints e.g. 3.14.1
184outlook-assistant --help # usage, options and key environment variables
185```
186 
187With no arguments the server speaks the Model Context Protocol over stdio. It's
188normally launched by your MCP client rather than run by hand — started from a
189terminal it will simply wait on stdin.
190 
191### 2. Register an Azure App
192 
193You need a Microsoft Azure app registration to authenticate. See the **[Azure Setup Guide](docs/guides/azure-setup.md)** for a detailed walkthrough (including first-time Azure account creation), or if you've done this before:
194 
1951. Create a new app registration at [portal.azure.com](https://portal.azure.com/)
1962. Add Microsoft Graph delegated permissions (Mail, Calendar, Contacts)
1973. _(Browser flow only)_ Create a client secret and copy the **Value** (not the Secret ID). The default device-code sign-in doesn't need one
1984. Under Authentication > **Add a platform** > **Mobile and desktop applications** — check `nativeclient` URI
1995. Enable **"Allow public client flows"** in Authentication > Advanced settings
2006. _(Optional)_ Set redirect URI to `http://localhost:3333/auth/callback` — only needed for browser auth flow
201 
202### 3. Configure Your MCP Client
203 
204**Client support.** Every MCP client gets the server's own checks. The plugin adds the `using-outlook-assistant` skill and a safety hook in Claude Code, GitHub Copilot and Cursor, with different limits in each. See [Supported Clients and Their Limits](docs/how-to/getting-started/supported-clients.md).
205 
206**Plugin install.** The plugin ([`plugins/outlook-assistant`](plugins/outlook-assistant/)) bundles the server pinned to an exact version, the skill and the safety hook. It follows both the Claude Code plugin format and the [Agent Plugins](https://agent-plugins.org/) format used by GitHub Copilot, plus a Cursor manifest (`.cursor-plugin/`).
207 
208- **Claude Code.** The plugin asks for your settings when you enable it (client ID, sign-in audience, send limit per session, allowed recipients, read-only mode and confirmation level):
209 
210 ```bash
211 claude plugin marketplace add littlebearapps/outlook-assistant
212 claude plugin install outlook-assistant@littlebearapps
213 ```
214 
215- **GitHub Copilot CLI.** Copilot has no plugin settings, so give your client ID when you first sign in, and set the hook's confirmation level with the `OUTLOOK_CONFIRM_LEVEL` environment variable:
216 
217 ```bash
218 copilot plugin marketplace add littlebearapps/outlook-assistant
219 copilot plugin install outlook-assistant@littlebearapps
220 ```
221 
222 VS Code's Copilot agent reads the same plugin and hook file; that hasn't been checked by hand yet.
223 
224- **Cursor** (v3.14.0 or later). Cursor loads the folder as a Cursor plugin (`.cursor-plugin/plugin.json`). In Cursor CLI, load it from a clone of this repository with `cursor-agent --plugin-dir outlook-assistant/plugins/outlook-assistant`. Give your client ID when you first sign in. The v3.13.0 plugin can't sign in from Cursor (`AADSTS900023`); use the manual config below instead.
225 
226**Manual config.** Use this for Claude Desktop, Codex CLI, Gemini CLI, Windsurf and other MCP clients, or in place of a plugin (you then get no hook). Add to your MCP client config. Only `OUTLOOK_CLIENT_ID` is needed for the default device-code sign-in; add `OUTLOOK_CLIENT_SECRET` only if you use the [browser flow](#browser-redirect-flow-alternative). You can also leave the client ID out and give it to your assistant when you first connect (`auth action=authenticate clientId=…`), which saves it to `~/.outlook-assistant-config.json`. An `OUTLOOK_CLIENT_ID` in the environment always takes precedence.
227 
228<details>
229<summary><strong>Claude Desktop</strong> (<code>claude_desktop_config.json</code>)</summary>
230 
231```json
232{
233 "mcpServers": {
234 "outlook": {
235 "command": "npx",
236 "args": ["@littlebearapps/outlook-assistant"],
237 "env": {
238 "OUTLOOK_CLIENT_ID": "your-application-client-id"
239 }
240 }
241 }
242}
243```
244</details>
245 
246<details>
247<summary><strong>Claude Code</strong> (CLI)</summary>
248 
249```bash
250claude mcp add outlook \
251 -e OUTLOOK_CLIENT_ID=your-application-client-id \
252 -- npx -y @littlebearapps/outlook-assistant
253```
254 
255The MCP server reads its settings from the environment your client passes it; it doesn't load a `.env` file.
256</details>
257 
258<details>
259<summary><strong>VS Code / GitHub Copilot</strong> (<code>.vscode/mcp.json</code>)</summary>
260 
261VS Code prompts for the client ID the first time the server starts and stores it securely:
262 
263```json
264{
265 "inputs": [
266 {
267 "type": "promptString",
268 "id": "outlook-client-id",
269 "description": "Azure application (client) ID"
270 }
271 ],
272 "servers": {
273 "outlook": {
274 "type": "stdio",
275 "command": "npx",
276 "args": ["-y", "@littlebearapps/outlook-assistant"],
277 "env": {
278 "OUTLOOK_CLIENT_ID": "${input:outlook-client-id}"
279 }
280 }
281 }
282}
283```
284 
285Use it from Copilot Chat in **Agent** mode. To use it in every workspace, add the same entry to your user `mcp.json` (Command Palette → **MCP: Open User Configuration**).
286</details>
287 
288<details>
289<summary><strong>Cursor</strong> (<code>.cursor/mcp.json</code>)</summary>
290 
291[![Install in Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](cursor://anysphere.cursor-deeplink/mcp/install?name=Outlook%20Assistant&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkBsaXR0bGViZWFyYXBwcy9vdXRsb29rLWFzc2lzdGFudCJdLCJlbnYiOnsiT1VUTE9PS19DTElFTlRfSUQiOiIifX0=)
292 
293Or add manually to `.cursor/mcp.json`:
294 
295```json
296{
297 "mcpServers": {
298 "outlook": {
299 "command": "npx",
300 "args": ["@littlebearapps/outlook-assistant"],
301 "env": {
302 "OUTLOOK_CLIENT_ID": "your-application-client-id"
303 }
304 }
305 }
306}
307```
308</details>
309 
310<details>
311<summary><strong>Windsurf</strong> (<code>~/.codeium/windsurf/mcp_config.json</code>)</summary>
312 
313```json
314{
315 "mcpServers": {
316 "outlook": {
317 "command": "npx",
318 "args": ["@littlebearapps/outlook-assistant"],
319 "env": {
320 "OUTLOOK_CLIENT_ID": "your-application-client-id"
321 }
322 }
323 }
324}
325```
326</details>
327 
328### 4. Authenticate
329 
3301. Ask your AI assistant to connect to Outlook — it calls the `auth` tool with `action=authenticate` and returns a short code and the URL `microsoft.com/devicelogin`
3312. Open the URL on any device (a private/incognito window avoids cached sessions), enter the code, sign in and grant permissions
3323. Tell your assistant you're done — it calls `auth` with `action=device-code-complete`
3334. Tokens are saved locally and refresh automatically
334 
335No auth server is needed for this default device-code flow. If you'd rather use the browser redirect flow, see [Authentication Flow](#authentication-flow) below.
336 
337## Installation
338 
339### Prerequisites
340 
341- **Node.js** 18.18.0 or higher (contributors: the dev tooling needs 22.22.1 or higher)
342- **npm** (included with Node.js)
343- **Azure account** for app registration ([free tier works](https://azure.microsoft.com/free/))
344 
345### From npm (recommended)
346 
347```bash
348npm install -g @littlebearapps/outlook-assistant
349```
350 
351### From source
352 
353```bash
354git clone https://github.com/littlebearapps/outlook-assistant.git
355cd outlook-assistant
356npm install
357```
358 
359### CLI options
360 
361| Option | What it does |
362|--------|-------------|
363| `-v`, `--version` | Print the version to stdout and exit 0 |
364| `-h`, `--help` | Print usage, options and key environment variables, and exit 0 |
365| _(none)_ | Start the MCP server on stdio — the normal mode, invoked by your MCP client |
366 
367An unrecognised argument is reported on stderr and exits 1, rather than starting
368a server that would ignore it.
369 
370## Azure App Registration
371 
372> **First time with Azure?** The [Azure Setup Guide](docs/guides/azure-setup.md) covers everything from creating an account to your first authentication, including billing setup and common pitfalls.
373 
374### Create the App
375 
3761. Open [Azure Portal](https://portal.azure.com/)
3772. Sign in with a Microsoft Work or Personal account
3783. Search for **App registrations** and click **New registration**
3794. Enter a name (e.g. "Outlook Assistant Server")
3805. Select **Accounts in any organizational directory and personal Microsoft accounts**
3816. Set redirect URI: platform **Web**, URI `http://localhost:3333/auth/callback`
3827. Click **Register**
3838. Copy the **Application (client) ID**
384 
385### Add Permissions
386 
3871. Go to **API permissions** > **Add a permission** > **Microsoft Graph** > **Delegated permissions**
3882. Add these **required** permissions:
389 - `offline_access` — refresh tokens between sessions
390 - `User.Read` — basic profile
391 - `Mail.Read`, `Mail.ReadWrite`, `Mail.Send` — email operations
392 - `Calendars.Read`, `Calendars.ReadWrite` — calendar operations
393 - `Contacts.Read`, `Contacts.ReadWrite` — contact management
394 - `MailboxSettings.ReadWrite` — settings, auto-replies, categories
395 - `People.Read` — people search
3963. Optionally add **org-only** permissions (work/school accounts only):
397 - `Mail.Read.Shared` — shared mailbox read access (requested only when `OUTLOOK_SHARED_MAILBOX=read` or `=true`)
398 - `Mail.ReadWrite.Shared` — shared mailbox writes (move/categorise/flag/mark-read; requested only when `OUTLOOK_SHARED_MAILBOX=true`)
399 - `Place.Read.All` — meeting room search (requires admin consent)
4004. Click **Add permissions**
401 
402### Create a Client Secret
403 
404Only needed for the [browser redirect flow](#browser-redirect-flow-alternative). Skip this if you sign in with the default device code.
405 
4061. Go to **Certificates & secrets** > **New client secret**
4072. Enter a description and select expiration
4083. Click **Add**
4094. **Copy the secret Value immediately** — you won't be able to see it again. Use the **Value**, not the Secret ID.
410 
411## Configuration
412 
413### Environment Variables
414 
415Set these in your MCP client's `"env"` block (see [Quick Start](#3-configure-your-mcp-client)). The MCP server doesn't load `.env` files; the browser-flow auth server (`npm run auth-server`) does, so when running from source you can also keep a `.env` for it:
416 
417```bash
418cp .env.example .env
419```
420 
421Edit with your Azure credentials:
422 
423```bash
424OUTLOOK_CLIENT_ID=your-application-client-id
425OUTLOOK_CLIENT_SECRET=your-client-secret-VALUE
426USE_TEST_MODE=false
427```
428 
429> **Note:** The server also accepts `MS_CLIENT_ID` and `MS_CLIENT_SECRET` for backwards compatibility.
430 
431**Optional overrides** (v3.8.0+) — see [`.env.example`](.env.example) for the full list with commented worked examples:
432 
433| Variable | Purpose | Default |
434|----------|---------|---------|
435| `OUTLOOK_AUTH_AUDIENCE` | OAuth audience: `common`, `consumers` (personal-only Azure apps), `organizations`, or single-tenant GUID. Fixes `AADSTS9002331` for personal-only app registrations. | `common` |
436| `OUTLOOK_DEFAULT_TIMEZONE` | IANA timezone applied to calendar events when callers don't pass one (e.g. `Europe/London`, `America/New_York`). | `Australia/Melbourne` |
437| `OUTLOOK_MAX_EMAILS_PER_SESSION` | Default per-session cap for each rate-limited tool, counted separately until the server restarts: `send-email` (including `draft action=send`), `draft` create/update/reply/reply-all/forward, `manage-rules` and `create-event`. Override one tool with `OUTLOOK_MAX_<TOOL>_PER_SESSION`, e.g. `OUTLOOK_MAX_SEND_EMAIL_PER_SESSION`. Unset or empty means no limit; **`0` blocks the tool** (before v3.14.1, `0` meant no limit), and so does any value that isn't a whole number. | no limit |
438| `OUTLOOK_ALLOWED_RECIPIENTS` | Comma-separated allowlist of domains/addresses for sends, drafts, rule forwards and calendar invitations (`create-event` and `manage-event` update attendees). Not applied to cancellation/decline messages or automatic replies. | unrestricted |
439| `OUTLOOK_SHARED_MAILBOX` | Opt-in shared-mailbox support (work/school only). `read` requests `Mail.Read.Shared`; `true` (or `readwrite`/`1`) also requests `Mail.ReadWrite.Shared`. Unset leaves sign-in unchanged. After enabling, restart and run `auth action=authenticate force=true`. | unset (off) |
440| `OUTLOOK_SEARCH_SCAN_LIMIT` | How many recent messages the client-side search fallback scans. Personal accounts match `to` locally within this window, so the default caps how far back a `to` search reaches. Max 5000. | `500` |
441| `OUTLOOK_REQUEST_TIMEOUT_MS` | Inactivity timeout for each Graph request attempt, in milliseconds: an attempt that receives no data for this long is abandoned with a timeout error. It isn't an overall deadline, so a slow response that keeps arriving isn't cut off. Throttled (`429`) and busy (`503`/`504`) responses are retried automatically, honouring `Retry-After`. | `60000` |
442| `OUTLOOK_READ_ONLY` | Read-only mode: `true` (or `1`/`yes`/`on`) refuses every tool call or action that isn't a read, including dry runs, exports and attachment downloads, before it runs. Signing in still works. An unrecognised value also turns it on, with a warning. Restart the server after changing it. | off |
443| `OUTLOOK_DEBUG` | Detailed stderr logs: `true` (or `1`/`yes`/`on`) adds search strategies, subjects, folder names and Graph error bodies, with email addresses and long IDs redacted. Off, each tool call logs one line (tool, action, outcome, duration) and never its arguments. Tokens, device codes and secrets are never logged. See [Server Logs and Debug Logging](docs/troubleshooting.md#server-logs-and-debug-logging). | off |
444| `OUTLOOK_EXPORT_DIR` | Extra folder that `export` and `attachments` downloads may write into. Without it, files can only go to the system temp directory, `~/Downloads` or `~/Documents`; other paths are refused. Absolute path (a leading `~` is expanded). | unset |
445 
446`OUTLOOK_CONFIRM_LEVEL` (`outward`, `all-writes` or `off`; default `outward`) isn't a server setting: the plugin's safety hook reads it, in clients with no plugin settings (GitHub Copilot, VS Code, Cursor). Set it in the environment the client starts from, not in the server's `env` block. In Claude Code, use the plugin's **Confirmation level** setting instead. See [Supported Clients and Their Limits](docs/how-to/getting-started/supported-clients.md).
447 
448### MCP Client Configuration
449 
450See [Quick Start — Configure Your MCP Client](#3-configure-your-mcp-client) above for the plugin installs and the Claude Desktop, Claude Code, VS Code / GitHub Copilot, Cursor, and Windsurf configs.
451 
452If installed from source, use `node` instead of `npx`:
453 
454```json
455{
456 "mcpServers": {
457 "outlook": {
458 "command": "node",
459 "args": ["/path/to/outlook-assistant/index.js"],
460 "env": {
461 "OUTLOOK_CLIENT_ID": "your-application-client-id"
462 }
463 }
464 }
465}
466```
467 
468## Authentication Flow
469 
470### Device Code Flow (Default — Recommended)
471 
472No auth server needed. Works everywhere, including remote/headless environments.
473 
4741. Ask your AI assistant to authenticate (calls `auth` tool with `action=authenticate`)
4752. Visit the URL shown (`microsoft.com/devicelogin`) on **any** browser, **any** device
4763. Enter the code, sign in with your Microsoft account, and grant permissions
4774. Tell your AI assistant to complete authentication (calls `auth` with `action=device-code-complete`)
4785. Tokens are saved to `~/.outlook-assistant-tokens.json` and **refresh automatically**
479 
480> **Prerequisite**: Enable "Allow public client flows" in Azure Portal > your app > Authentication > Advanced settings.
481>
482> **Server restarts** (v3.7.2+): Device code state is persisted to `~/.outlook-assistant-pending-auth.json`, so `device-code-complete` works even if the MCP server restarts between steps 1 and 4 (e.g., Untether/Telegram bridge, Claude Desktop session changes).
483 
484### Browser Redirect Flow (Alternative)
485 
486For localhost development or if you prefer the traditional OAuth flow, start the auth server. From a source checkout:
487 
488```bash
489npm run auth-server
490```
491 
492From a global npm install:
493 
494```bash
495node "$(npm root -g)/@littlebearapps/outlook-assistant/outlook-auth-server.js"
496```
497 
498This starts a local server on port 3333 to handle the OAuth callback. (The `outlook-assistant` command itself only accepts `--version` and `--help`; any other argument exits with an error.)
499 
5001. In your AI assistant, use the `auth` tool with `action=authenticate, method=browser`
5012. Open the provided URL in your browser
5023. Sign in and grant permissions — tokens are saved automatically
503 
504> **Note**: The auth server reads `OUTLOOK_CLIENT_ID` and `OUTLOOK_CLIENT_SECRET` from environment variables or a `.env` file in the directory you start it from. Your MCP client's `"env"` config only applies to the MCP server process, not a separately-started auth server.
505>
506> **Shared mailboxes**: the browser flow requests the configured scopes with no fallback. If you enable `OUTLOOK_SHARED_MAILBOX`, sign in with the device-code flow.
507 
508## Directory Structure
509 
510```
511outlook-assistant/
512├── index.js # Entry point: CLI flags, stdio transport
513├── server.js # MCP server factory (capabilities, request handler)
514├── tools.js # Tool registry (22 tools)
515├── request-handler.js # Routes MCP requests; JSON-RPC errors for unknown methods/tools
516├── config.js # Configuration settings
517├── outlook-auth-server.js # OAuth server (port 3333)
518├── auth/ # Authentication module (1 tool)
519├── email/ # Email module (8 tools)
520│ ├── mail-tips.js # Pre-send recipient validation
521│ ├── headers.js # Email header retrieval
522│ ├── mime.js # Raw MIME/EML content
523│ ├── conversations.js # Thread listing/export
524│ ├── attachments.js # Attachment operations
525│ └── ...
526├── calendar/ # Calendar module (3 tools)
527│ ├── attendees.js # Attendee builder (email or {email, type})
528│ └── list.js # list-events filters
529├── contacts/ # Contacts module (2 tools)
530├── categories/ # Categories module (3 tools)
531├── settings/ # Settings module (1 tool)
532├── folder/ # Folder module (1 tool; resolve.js resolves paths/IDs)
533├── rules/ # Rules module (1 tool)
534├── advanced/ # Advanced module (2 tools)
535└── utils/
536 ├── graph-api.js # Microsoft Graph API client (includes $batch, path guards)
537 ├── mailbox.js # me vs users/{sharedMailbox} prefix, shared-mailbox opt-in
538 ├── risk-classes.js # Risk class per tool/action; derives annotations and titles
539 ├── tool-error.js # isError tool results with a next step
540 ├── safety.js # Rate limiting, recipient allowlist, dry-run
541 ├── safe-write.js # Exclusive, folder-confined file writes
542 ├── datetime.js # ISO 8601 parsing and timezone conversion
543 ├── odata-helpers.js # OData query building
544 ├── field-presets.js # Token-efficient field selections
545 ├── response-formatter.js # Verbosity levels
546 └── mock-data.js # Test mode data
547```
548 
549## Troubleshooting
550 
551### "Cannot find module '@modelcontextprotocol/sdk/server/index.js'"
552 
553```bash
554npm install
555```
556 
557### "EADDRINUSE: address already in use :::3333"
558 
559```bash
560npx kill-port 3333
561npm run auth-server
562```
563 
564### "Invalid client secret" (AADSTS7000215)
565 
566You're using the Secret **ID** instead of the Secret **Value**. Go to Azure Portal > Certificates & secrets and copy the **Value** column into `OUTLOOK_CLIENT_SECRET`.
567 
568The Value is shown only once, when the secret is created — if you've navigated away it can't be read again, so create a new secret. An **expired** secret produces this same error, so check the Expires column too.
569 
570Since v3.11.0 the server detects this error and appends the explanation to Microsoft's original message, so you see both the raw error code and what to do about it.
571 
572### Authentication URL doesn't work
573 
574If using browser flow: start the auth server first with `npm run auth-server`. If using device code flow: visit `microsoft.com/devicelogin` instead.
575 
576### Device code "invalid_client"
577 
578Enable "Allow public client flows" in Azure Portal > App registrations > Authentication > Advanced settings.
579 
580### Token refresh fails after ~60 minutes (device code auth)
581 
582Fixed in v3.7.2. Earlier versions sent `client_secret` in token refresh requests for device-code auth, which Microsoft rejects for public client flows. Update to v3.7.2+ or re-authenticate.
583 
584### "Authentication required."
585 
586You're signed out, or the saved token expired and couldn't be refreshed. The error says what to do next: sign in with the `auth` tool with `action=authenticate` (add `force=true` to replace an existing session), then retry the call. `auth action=status` shows the current state.
587 
588## Development
589 
590### Running Tests
591 
592```bash
593npm test # Jest unit tests
594npm run inspect # MCP Inspector (interactive)
595```
596 
597### Test Mode
598 
599Run with mock data (no real API calls):
600 
601```bash
602USE_TEST_MODE=true npm start
603```
604 
605### Extending the Server
606 
6071. Create a new module directory (e.g. `tasks/`)
6082. Implement tool handlers in separate files
6093. Export tool definitions from the module's `index.js`
6104. Add the module's tools to the `TOOLS` array in `tools.js`
6115. Classify every tool and action in `utils/risk-classes.js` (a test fails on anything unclassified); the annotations and title come from there
6126. Add tests in `test/`
6137. Update `docs/quickrefs/tools-reference.md`
614 
615## Documentation
616 
617| Guide | Description |
618|-------|-------------|
619| [Getting Started](docs/how-to/getting-started/connect-outlook-to-claude.md) | Install, configure, and authenticate — start here |
620| [Supported Clients](docs/how-to/getting-started/supported-clients.md) | Install per client, what the skill and safety hook do in each, and known limits |
621| [Azure Setup Guide](docs/guides/azure-setup.md) | Azure account creation, app registration, permissions, and secrets |
622| [How-To Guides](docs/how-to/index.md) | 30 practical guides for email, calendar, contacts, and settings |
623| [Roadmap](ROADMAP.md) | Active milestones (v3.14.1, v3.15.0, v4.0.0, v3.8.x, v3.16.0+) and recent releases |
624| [Troubleshooting](docs/troubleshooting.md) | Known errors and fixes, including auth, search, export and shared mailboxes |
625| [FAQ](docs/faq/faq.md) | Install, accounts, permissions, tokens, updates, uninstall |
626| [Tools Reference](docs/quickrefs/tools-reference.md) | All 22 tools with parameters |
627| [AI Agent Guide](docs/how-to/ai-agents/using-outlook-assistant-in-agents.md) | Tool selection and workflow patterns for AI agents |
628 
629Full documentation: [docs/](docs/README.md)
630 
631## Known Limitations
632 
633- **Personal account search**: Free-text `query` and the raw `searchExpression` (formerly `kqlQuery`) rely on Microsoft's `$search` API, which has limited support on personal Outlook.com accounts. `query` mitigates this with progressive fallback (OData filters, boolean filters, then a client-side scan). Field-scoped `$search` (e.g. `subject:"…"`) is rejected outright there; since v3.10.0 `from:`/`to:`/`subject:` expressions are translated into the closest equivalent OData filters and retried, but boolean operators, grouping, wildcards and other field prefixes are not — those still terminate with an explicit no-results rather than a silent broader search. Structured filters (`from`, `subject`, `to`, `receivedAfter`) remain the most direct route. Cross-folder search (`searchAllFolders: true`) returns a superset of inbox-only results. Note that `query` and `searchExpression` are not interchangeable there: `searchExpression` goes to `$search`, which matches the whole message including the body and ranks by relevance rather than date, while `query` falls back to a subject substring match that never reads bodies.
634- **`to` search depth on personal accounts**: the server-side recipient filter is rejected, so `to` is matched locally over the 500 most recent messages (`OUTLOOK_SEARCH_SCAN_LIMIT`, max 5000). On a large archive that excludes older mail — pair `to` with `receivedAfter`/`receivedBefore`. Since v3.11.1 the response says so whenever the scan was truncated, whether or not it matched.
635- **Focused Inbox**: Only available on work/school Microsoft 365 accounts.
636- **Shared mailboxes**: Require a work/school account and are **opt-in**: set `OUTLOOK_SHARED_MAILBOX=read` (read) or `=true` (read and organise), restart the server, then re-authenticate with `auth action=authenticate force=true`. Until then, `sharedMailbox` calls are refused with setup guidance (`access-shared-mailbox` keeps its previous well-known-folder behaviour). `auth action=about` shows whether the shared scopes were actually granted. Support covers reading and organising only. Reading needs `Mail.Read.Shared`; organising (move/categorise/flag/mark-read/create folders via `sharedMailbox`) needs `Mail.ReadWrite.Shared` — add it in Azure and re-authenticate (until then, shared-scoped writes fail with 403; they never fall back to your own mailbox). Custom subfolders are supported — pass `folder` as a display name or nested path (e.g. `Inbox/Vendors/Acme`), a raw `folderId`, or use `listFolders: true` (or `folders action=list, sharedMailbox: …`) to discover them. **Sending, drafts, replies, and forwards from a shared mailbox are not supported** — `send-email` and `draft` (including reply/reply-all/forward) always act on the signed-in user's own mailbox, and `Mail.Send.Shared` is not requested.
637- **Meeting room search**: Requires `Place.Read.All` permission with admin consent (work/school accounts only).
638- **Export default path**: Exports and attachment downloads save to the system temp directory by default (a batch `export` with `target=messages` needs an `outputDir`). Use `outputDir` (or `savePath`) with an absolute path (or one starting with `~/`) inside the system temp directory, `~/Downloads`, `~/Documents` or `OUTLOOK_EXPORT_DIR`; relative paths and other folders are refused. An existing `savePath` file is replaced only with `overwrite: true`.
639- **`list-events` date filters**: `startAfter`/`startBefore` must include `Z` or a ±hh:mm offset; zone-less and date-only values are rejected rather than guessed.
640 
641## Contributing
642 
643Contributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
644 
645## Security
646 
647For security concerns, please see our [Security Policy](SECURITY.md). Do not open public issues for vulnerabilities.
648 
649## Changelog
650 
651See [CHANGELOG.md](CHANGELOG.md) for version history.
652 
653## About
654 
655Built and maintained by [Little Bear Apps](https://littlebearapps.com). Outlook Assistant is open source under the [MIT License](LICENSE).
656 

Discussion

Alternatives