outlook-classic-mcp-hardened
by wa1939
README.md
# outlook-classic-mcp-hardened
[](LICENSE)
[](https://github.com/wa1939/outlook-classic-mcp-hardened)
[](pyproject.toml)
[](https://modelcontextprotocol.io)
A **local MCP server** that gives AI agents full, safe control of the **classic Outlook desktop client** on Windows — mail, calendar, contacts, tasks, categories, and rules — through the COM API, with **no Microsoft Graph, no OAuth, no Azure/Entra app registration, and no cloud email API**.
Built for the environment most agent integrations forget: **locked-down corporate, government, and university Microsoft 365 tenants** where Graph access is disabled, app registration is forbidden, and OAuth integrations are blocked by policy. If Outlook is installed on the machine, this works — because it talks to Outlook exactly the way Office add-ins and macros do.
## Why this exists
Most "give my agent email access" projects are Graph API wrappers. That fails in restricted environments:
| Usual approach | Restricted tenant reality |
|---|---|
| Microsoft Graph API | Disabled or unlicensed for apps; requires admin consent |
| OAuth / Azure app registration | Blocked by tenant policy; security review takes months |
| IMAP/SMTP | Frequently disabled tenant-wide |
| **Outlook COM (this project)** | **Works. It is the same surface macros and add-ins use, on your own machine, under your own account** |
Authentication piggybacks on whatever account the Outlook desktop client is already signed into. Nothing leaves the machine except the emails your agent drafts and you approve.
## The hardening: draft-first by design
An agent with an unsupervised `Send()` is a risk no security team should accept. This fork makes the safe path the default path:
1. **Draft-first default** — `outlook_send_mail`, `outlook_reply_mail`, and `outlook_forward_mail` save to **Drafts** by default. Nothing is ever sent implicitly.
2. **Double gate for real sending** — an actual send requires *both* the per-call `confirm_send=True` parameter *and* `OUTLOOK_MCP_ALLOW_SEND=1` in the server environment. Either alone is not enough.
3. **Recipient-domain allowlist** — set `OUTLOOK_MCP_ALLOWED_DOMAINS=yourcompany.com,partner.com` and outbound sends to any other domain are blocked at the COM layer. Draft creation still works everywhere.
4. **Safe rules only** — the rule tools create triage rules (move by subject/sender) and deliberately cannot create auto-forward rules, a classic exfiltration path.
5. **No network surface** — the server is stdio-only. It speaks JSON-RPC to the agent on stdin/stdout and COM to Outlook. It has no ports, no HTTP endpoint, no telemetry.
The intended workflow:
```
agent creates draft → you review recipients/subject/body → agent sends after your approval
outlook_create_draft → outlook_get_mail (review) → outlook_send_draft(confirm_send=True)
```
## Requirements
- Windows 10 or 11
- **Outlook desktop (Classic)** — the `OUTLOOK.EXE` from Microsoft 365 / Office. The "new Outlook" (`olk.exe`) does **not** work (no COM surface).
- Python 3.10+
You do not need Outlook to be open — the server launches it on the first COM call.
## Install
```bat
git clone https://github.com/wa1939/outlook-classic-mcp-hardened.git
cd outlook-classic-mcp-hardened
python -m venv .venv
.venv\Scripts\python -m pip install -U pip
.venv\Scripts\pip install -e .
```
Or install straight from GitHub:
```
pip install git+https://github.com/wa1939/outlook-classic-mcp-hardened.git
```
Smoke-test it (Ctrl+C to stop):
```bat
.venv\Scripts\python -m outlook_mcp
```
> **Windows + COM note:** some launchers pass a minimal environment to MCP subprocesses, which can break Python's socket/asyncio modules (`WinError 10106`). If the server fails to connect from your agent, pass the standard Windows variables (`SystemRoot`, `COMSPEC`, `USERPROFILE`, `APPDATA`, `LOCALAPPDATA`, `TEMP`, `TMP`) in the server's `env` block — the configs below already include them where needed.
## Connect your agent
Use the **absolute path** to the `.venv\Scripts\python.exe` you just created. Replace `C:\path\to\outlook-classic-mcp-hardened` everywhere below.
### Hermes Agent
One command (accepts the registration prompt automatically):
```bash
printf 'Y\n' | hermes mcp add outlook \
--command C:/path/to/outlook-classic-mcp-hardened/.venv/Scripts/python.exe \
--env OUTLOOK_MCP_ALLOW_SEND=0 \
SystemRoot=C:/Windows SYSTEMROOT=C:/Windows \
windir=C:/Windows WINDIR=C:/Windows \
COMSPEC=C:/Windows/system32/cmd.exe \
USERPROFILE=C:/Users/YOURNAME \
APPDATA=C:/Users/YOURNAME/AppData/Roaming \
LOCALAPPDATA=C:/Users/YOURNAME/AppData/Local \
TEMP=C:/Users/YOURNAME/AppData/Local/Temp \
TMP=C:/Users/YOURNAME/AppData/Local/Temp \
--args -m outlook_mcp
```
Verify and restart:
```bash
hermes mcp list
hermes mcp test outlook
# then start a new Hermes session (or /reload-mcp) — tools appear as mcp_outlook_*
```
The Windows environment variables are not optional decoration: without them, Python COM subprocesses can fail to start under Hermes' filtered environment.
### Claude Code
```bash
claude mcp add outlook \
-e OUTLOOK_MCP_ALLOW_SEND=0 \
-- C:/path/to/outlook-classic-mcp-hardened/.venv/Scripts/python.exe -m outlook_mcp
```
Add `-s user` to register it for all projects instead of the current one. If the server fails to connect, append the Windows env vars the same way (`-e SystemRoot=C:/Windows -e COMSPEC=...`).
### Codex CLI
```bash
codex mcp add outlook -- \
C:/path/to/outlook-classic-mcp-hardened/.venv/Scripts/python.exe -m outlook_mcp
```
Add environment variables with repeated `--env KEY=VALUE` flags if needed.
### Claude Desktop / Cursor / Windsurf / any mcpServers config
```json
{
"mcpServers": {
"outlook": {
"command": "C:/path/to/outlook-classic-mcp-hardened/.venv/Scripts/python.exe",
"args": ["-m", "outlook_mcp"],
"env": {
"OUTLOOK_MCP_ALLOW_SEND": "0",
"OUTLOOK_MCP_ALLOWED_DOMAINS": "yourcompany.com",
"SystemRoot": "C:/Windows",
"SYSTEMROOT": "C:/Windows",
"windir": "C:/Windows",
"WINDIR": "C:/Windows",
"COMSPEC": "C:/Windows/system32/cmd.exe",
"USERPROFILE": "C:/Users/YOURNAME",
"APPDATA": "C:/Users/YOURNAME/AppData/Roaming",
"LOCALAPPDATA": "C:/Users/YOURNAME/AppData/Local",
"TEMP": "C:/Users/YOURNAME/AppData/Local/Temp",
"TMP": "C:/Users/YOURNAME/AppData/Local/Temp"
}
}
}
}
```
A ready-to-edit copy lives at [`examples/mcp-config-example.json`](examples/mcp-config-example.json).
## Security configuration
| Variable | Default | Effect |
|---|---|---|
| `OUTLOOK_MCP_ALLOW_SEND` | `0` (off) | Master send gate. Must be `1` for any real send, **and** the tool call must also pass `confirm_send=true`. |
| `OUTLOOK_MCP_ALLOWED_DOMAINS` (or `OUTLOOK_MCP_ALLOWED_RECIPIENT_DOMAINS`) | unset | Comma-separated allowlist of recipient domains. Sends outside the list are blocked; drafts are unaffected. |
| `OUTLOOK_MCP_ALLOW_UNRESOLVED_RECIPIENTS` | `0` (off) | Set to `1` to allow sending to recipients Outlook cannot resolve to a real SMTP address. Leave it off. |
| `OUTLOOK_MCP_LOG` | `INFO` | Log level (logs go to stderr only — never stdout, which carries the MCP stream). |
Check the live policy any time with the `outlook_get_send_policy` tool.
## Tools (38)
| Area | Tools |
|---|---|
| **Mail** | `outlook_list_mails`, `outlook_get_mail`, `outlook_search_mails`, `outlook_create_draft`, `outlook_send_draft`, `outlook_send_mail`, `outlook_reply_mail`, `outlook_forward_mail`, `outlook_mark_mail`, `outlook_move_mail`, `outlook_delete_mail`, `outlook_save_attachments`, `outlook_get_send_policy` |
| **Folders** | `outlook_list_folders`, `outlook_create_folder` |
| **Calendar** | `outlook_list_events`, `outlook_get_event`, `outlook_create_event`, `outlook_update_event`, `outlook_delete_event`, `outlook_respond_event` |
| **Contacts** | `outlook_list_contacts`, `outlook_get_contact`, `outlook_search_contacts`, `outlook_resolve_name` |
| **Tasks** | `outlook_list_tasks`, `outlook_create_task`, `outlook_update_task`, `outlook_complete_task`, `outlook_delete_task` |
| **Categories** | `outlook_list_categories`, `outlook_set_category` |
| **Rules** | `outlook_list_rules`, `outlook_create_move_rule`, `outlook_toggle_rule`, `outlook_delete_rule` |
| **Account** | `outlook_whoami`, `outlook_get_out_of_office` |
Destructive tools (`outlook_delete_task`, `outlook_delete_rule`) require an explicit `confirm_delete=true` on every call.
## Recommended agent rule
Put this in your agent's instructions:
> Use Outlook read/search tools freely. For outbound email, always create a draft first, show me the exact recipients, subject, and body, and only call `outlook_send_draft(..., confirm_send=true)` after I approve.
## Documentation
- [`docs/HARDENING.md`](docs/HARDENING.md) — what this fork changes relative to upstream
- [`docs/PATCH_DIFF.patch`](docs/PATCH_DIFF.patch) — the exact patch against upstream 0.3.1
- [`README_UPSTREAM.md`](README_UPSTREAM.md) — the original project's README
- [`CHANGELOG_HARDENED.md`](CHANGELOG_HARDENED.md) — hardening changelog
## Credits & license
Based on the MIT-licensed [outlook-classic-mcp](https://github.com/anasahmed07/Outlook-Classic-MCP) by **Anas Shaikh**. This fork hardens the outbound-mail flow (draft-first defaults, send gating, domain allowlist) and extends tasks and rules; see [`docs/HARDENING.md`](docs/HARDENING.md).
Released under the [MIT License](LICENSE) with attribution to both authors. Microsoft Outlook is a trademark of Microsoft Corporation; this project is not affiliated with or endorsed by Microsoft.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues