{
  "markdown": "# iris\n\n**A Microsoft 365 mail server for AI agents that does not send email — until you decide it should.**\n\nBy default iris requests the delegated Graph scope `Mail.ReadWrite` and never\n`Mail.Send`. The access token it holds has no capability to transmit a message,\nso no prompt, no jailbreak and no bug in this code can make one go out. It writes\ndrafts into a folder in your mailbox; you open Outlook and press Send.\n\nSending is an explicit opt-in (`IRIS_ENABLE_SEND=1`) that requires granting the\n`Mail.Send` scope and re-consenting — and even then every send is human-confirmed\nand allowlist-checked. Leave it off (the default) and the no-send property is\nstructural, as above. See [Enabling send](#enabling-send).\n\nThat is the whole design. Everything else is detail.\n\n---\n\n## Why this shape\n\nThe usual worry about giving an agent your mailbox is that it will send\nsomething you did not sanction — to the wrong person, with the wrong tone, or\nbecause someone talked it into doing so. The common answer is a confirmation\nprompt, which is a guardrail: code that asks permission, and code can be\nbypassed.\n\niris removes the capability instead. Microsoft Graph will reject a send attempt\nmade with this token, because the consent screen you approved never included\nthat permission. The security boundary is Microsoft's, not this program's, and\nit holds even if this program is wrong.\n\nThe trade is real: a human is in the loop on every message, by construction. If\nyou want autonomous sending, iris is the wrong tool.\n\n## Install\n\n```bash\nuvx iris-mcp        # run without installing\npip install iris-mcp\n```\n\n**Python 3.10 or newer.** macOS ships Python 3.9, which is too old — `mcp`\nrequires 3.10+. Use [`uv`](https://docs.astral.sh/uv/) (it comes with `uvx` and\nmanages its own Python), or install a current Python with Homebrew\n(`brew install python@3.12`). The system `python3` on macOS will not work.\n\n## Setup\n\n**You must register your own Entra application.** There is no shared app\nregistration and no hosted service — iris talks directly from your machine to\nyour tenant. This is deliberate: a shared app would mean trusting someone\nelse's client ID with access to your mail.\n\n1. Entra admin centre → **App registrations** → **New registration**. Single\n   tenant is fine. No redirect URI needed.\n2. **Authentication** → Settings → enable **Allow public client flows**. Device\n   code sign-in needs this. No client secret is used anywhere.\n3. **API permissions** → Microsoft Graph → **Delegated** → add\n   **`Mail.ReadWrite`**. For draft-only use (the default), add nothing else and\n   leave `Mail.Send` off — that omission is what makes the no-send property\n   structural. Add `Mail.Send` only if you intend to enable sending (see\n   [Enabling send](#enabling-send)).\n4. Copy the **Application (client) ID** and **Directory (tenant) ID**. Neither\n   is a secret.\n\nThen add iris to your MCP client:\n\n```json\n{\n  \"mcpServers\": {\n    \"iris\": {\n      \"command\": \"uvx\",\n      \"args\": [\"iris-mcp\"],\n      \"env\": {\n        \"IRIS_CLIENT_ID\": \"<application (client) id>\",\n        \"IRIS_TENANT_ID\": \"<directory (tenant) id>\"\n      }\n    }\n  }\n}\n```\n\nSign in once: call `iris_login`, open the URL, enter the code, then call\n`iris_login_finish`. The token cache is written next to the server, mode 600.\n\n**Deploying to a whole team?** See the [Administrator Deployment\nGuide](docs/ADMIN-DEPLOYMENT.md) — authorizing iris across a Microsoft 365 tenant:\nadmin consent, per-user assignment, and why it stays the delegated model rather\nthan application permissions.\n\n## Clients\n\niris is a local **stdio** MCP server: your MCP client launches it as a child\nprocess on the same machine. It works with any client that supports local stdio\nservers — Claude Desktop, Cursor, and the **Grok CLI**\n(`grok mcp add iris -- uvx iris-mcp`) among them.\n\nIt does **not** work with clients that only accept remote MCP connectors over\nHTTP. The **Grok app / Grok Bot** is in that category — it takes hosted HTTP\nservers, not local stdio ones — so iris cannot attach to it as-is. Bridging iris\nto an HTTP transport is possible, but out of scope for this project.\n\n## Tools\n\n| Tool | What it does |\n|---|---|\n| `iris_login` | Starts device-code sign-in, returns a URL and a code |\n| `iris_login_finish` | Completes sign-in; safe to call repeatedly while you type the code |\n| `iris_auth_status` | Who is signed in, which scopes, and whether Graph is reachable |\n| `iris_list_folders` | Lists your top-level mail folders, so you can pick one for a draft |\n| `iris_create_draft` | Writes a draft (to/cc/bcc, subject, body or HTML, optional reply-to, optional `folder`) |\n| `iris_list_drafts` | Lists what is waiting in a draft folder (optional `folder`) |\n| `iris_update_draft` | Revises a draft in place |\n| `iris_delete_draft` | Deletes a draft; requires `confirm=true` |\n| `iris_send_draft` | **Only present when `IRIS_ENABLE_SEND=1`.** Sends an existing draft; requires `confirm=true`, re-checks the allowlist |\n\n## Where drafts go\n\nInto a dedicated top-level mail folder, `AI Drafts` by default\n(`IRIS_DRAFT_FOLDER`), created on first use. Set the variable to an empty string\nto use the normal Drafts folder instead.\n\nYou can also choose the folder per draft: pass `folder` to `iris_create_draft`\n(and `iris_list_drafts`) to target any folder by name, created on first use if it\ndoes not exist. Pass `\"\"` or `\"Drafts\"` for the normal Outlook Drafts folder, or\ncall `iris_list_folders` first to pick from what already exists. Omitting\n`folder` uses the `IRIS_DRAFT_FOLDER` default, so nothing changes for existing\nsetups.\n\nThese are real drafts and Outlook sends them normally — but because they live in\ntheir own folder, they do **not** appear in the Drafts view. That is the point:\nagent-written mail sits somewhere you have to go and look, rather than mixed in\nwith your own half-finished messages.\n\nOne wrinkle worth knowing: Graph's `createReply` always lands a reply in Drafts\nfirst, so iris moves it afterwards, and a move assigns a new message id.\n\n## Enabling send\n\nSending is off by default and, by design, takes three deliberate steps — miss any\none and iris still cannot send:\n\n1. In your Entra app registration, add **`Mail.Send`** (Delegated) alongside\n   `Mail.ReadWrite`.\n2. Set `IRIS_ENABLE_SEND=1` in the server's environment. Only then is `Mail.Send`\n   requested and the `iris_send_draft` tool registered at all.\n3. Run `iris_login` again to re-consent — the cached token predates the new scope\n   and will not carry it until you do.\n\nThen `iris_send_draft(draft_id, confirm=true)` sends an existing draft. It refuses\nwithout `confirm`, re-verifies the message is still an unsent draft, and re-runs\nthe recipient allowlist before sending. The flow stays compose → review → send;\niris never composes and sends in one shot.\n\nUnderstand the trade. With send off, \"cannot send\" is enforced by Microsoft\nagainst your consent and holds even if this code is wrong. With send on, the last\nline of defence is a per-call confirmation — a guardrail *in this code*, which is\nexactly the kind of check a bug or a cleverly-worded prompt can talk past. Enable\nit only where that weaker guarantee is acceptable.\n\n## Other controls\n\n- **Recipient allowlist** — `recipients.allow`, one address or domain per line.\n  Absent or empty means all recipients are permitted. Point `IRIS_ALLOWLIST`\n  elsewhere if you prefer.\n- **Kill switch** — create a `DISABLED` file beside the server, or set\n  `IRIS_DISABLED=1`, and every tool refuses.\n- **Audit log** — every call is appended to `audit.log` (`IRIS_AUDIT_LOG`).\n\n## Limits\n\nNo attachments. No shared or delegated mailboxes — `/me` only. No folder nesting\nvia `parentFolderId`. Sign-in is delegated device-code as a public client, so\nthe blast radius is exactly one mailbox: yours.\n\n## Security\n\nThe no-send guarantee, how to verify it yourself, and — just as important — what\niris *can* reach with `Mail.ReadWrite`: see [SECURITY.md](SECURITY.md).\n\n## License\n\nMIT — see [LICENSE](LICENSE).\n\n<!-- mcp-name: io.github.SuperAngryMonkey/iris -->\n",
  "bytes": 8076,
  "sha": "bc05cf82c759e35f0e6c102fc3e0c13f3432097bb07c606bcf0df6a479c55be9",
  "repo_slug": "superangrymonkey/iris",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_superangrymonkey_iris_ddd695eb/readme"
}