Skip to main content
Glama
wa1939

outlook-classic-mcp-hardened

by wa1939
README.md
# outlook-classic-mcp-hardened

[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
[![Platform](https://img.shields.io/badge/platform-Windows%2010%20%7C%2011-blue)](https://github.com/wa1939/outlook-classic-mcp-hardened)
[![Python](https://img.shields.io/badge/python-3.10%2B-blue)](pyproject.toml)
[![MCP](https://img.shields.io/badge/protocol-Model%20Context%20Protocol-green)](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.