A Claude Code plugin for Microsoft Graph (Outlook) that is stdlib-only, zero-dependency, and
zero-backend by design — just urllib + json, no SDK, no server process, no install friction.
It gives an agent these capabilities, with safety built into the structure, not the behaviour:
- Read Outlook mail — list/get messages and their headers, read-only.
- Author native Outlook message rules — create / list / verify / remove server-side
messageRules, so deterministic mail organisation lives in Outlook (runs even when nothing else is, visible and editable in Outlook's own UI, reversible by deleting one rule). A rule can file to a folder and/or assign a coloured category. - Manage master categories — list and create-if-absent the coloured labels a rule assigns, so they always render with a colour.
- Create category search folders — virtual
mailSearchFolderviews over a category, behind a separateMail.ReadWritesign-in tier. A search folder never moves or deletes mail. - Move messages — re-file one or more messages to a folder (
message-move), behind a separateMail.ReadWritesign-in tier (--mode messages). MOVE-only and reversible, with a--dry_runpreview; it never deletes (no delete-capable scope is ever requested).
Each skill is a thin wrapper over the stdlib kernel; the runtime catalog is the source of truth
(python3 -m msgraph.client describe). Names are prefixed msgraph- when invoked as slash commands.
| Skill | Scope it needs | What it does |
|---|---|---|
auth-login |
Mail.Read + MailboxSettings.Read (read, default), + MailboxSettings.ReadWrite (--mode rules), or Mail.ReadWrite (--mode folders / --mode messages) |
Device-code sign-in; caches the token at the XDG path and refreshes it silently. Run first. |
mail-list |
Mail.Read |
List recent inbox messages (concise/detailed, pagination default 25). |
mail-get |
Mail.Read |
Fetch one message incl. its internet headers (e.g. List-Unsubscribe). |
message-move |
Mail.ReadWrite (--mode messages) |
Move message(s) to a folder (MOVE only, never delete; --dry_run previews). Re-files backlog mail that incoming-only rules can't touch; reversible. |
rule-list |
MailboxSettings.Read |
Enumerate existing inbox rules agent-legibly. |
rule-verify |
Mail.Read |
Compute a candidate rule's read-only catch-set and record the verification gate. |
rule-create |
MailboxSettings.ReadWrite |
Install a verified rule that files to a folder and/or assigns a category; refuses unverified criteria. |
rule-remove |
MailboxSettings.ReadWrite |
Delete a rule by id (the reversibility primitive). |
category-list |
MailboxSettings.Read |
List the mailbox master categories (name + colour). |
category-ensure |
MailboxSettings.ReadWrite |
Create a coloured category if absent (idempotent). |
folder-list |
Mail.Read |
List the real mail folders as a nested tree (name, total/unread counts), read-only. |
searchfolder-list |
Mail.Read |
Enumerate existing virtual search folders agent-legibly. |
searchfolder-create |
Mail.ReadWrite (--mode folders) |
Create a virtual category search-folder view; never touches mail. |
searchfolder-remove |
Mail.ReadWrite (--mode folders) |
Delete a search folder by id (affects only the view, never mail). |
The Microsoft Graph + Outlook plugin space is crowded — but it is almost entirely Node MCP servers and SDK-based Python. A plugin you can read top-to-bottom, that pulls in nothing and runs nothing in the background, is the empty niche. The constraint is the differentiator: portable, auditable, and with no supply-chain surface beyond the standard library.
- Read-only by default. Auth requests the read scopes only (
Mail.Read+MailboxSettings.Read). On a clean account this token carries no write grant. Caveat: Microsoft consent is sticky/cumulative — once any write tier has been consented for the account+client, the token endpoint returns those write scopes on every token, including a later read-mode sign-in, so "structural read-only" holds only until the first write consent. The plugin warns on stderr when a sign-in's granted scopes are a write-capable superset of the requested mode. Seedocs/adr/0001-scope-isolation-one-app-vs-per-tier.md. - Scope ratchet. Each write capability is a separate, deliberately-consented tier: rule/category
authoring needs
MailboxSettings.ReadWrite; creating search folders needsMail.ReadWrite(--mode folders); moving messages needsMail.ReadWrite(--mode messages). Escalation is deliberate and auditable (the OAuth consent is the record); a read or rule-authoring token cannot move a message or create a search folder. - Never delete — by construction. No verb deletes a message and no delete-capable scope is ever
requested, so deletion is structurally impossible.
message-movemoves mail (reversible: move it back); it is the only per-message mutation, gated behind its own scope and a--dry_runpreview. - Verify before install. A candidate rule's real catch-set is computed read-only and shown
before the rule is created — a rule is never trusted in the abstract (Graph
headerContainsis coarse substring matching, so it must be checked against actual mail).message-move --dry_runapplies the same preview-then-act discipline to moves. - Reversible by construction. Rules file or label mail; they never delete. Removing one rule undoes the organisation. Search folders are virtual saved views — creating or removing one never moves or deletes a message. A move is undone by moving back.
Two-tier: plugin/ is the shippable payload; the repo root is the build/distribution repo.
.claude-plugin/marketplace.json # marketplace entry — points at ./plugin
plugin/.claude-plugin/plugin.json # plugin manifest
plugin/skills/<subject>-<verb>/SKILL.md # agent-facing commands (auth, mail-read, rule-*)
plugin/src/msgraph/ # stdlib kernel package (importable + runnable)
client.py # thin CLI entrypoint — argparse dispatch + `describe`
runtime.py # HTTP seam, token cache, markers, Graph primitives
catalog.py render.py graph.py verbs.py # TOOLS catalog · output shaping · Graph helpers · verbs
docs/AGENT-FRIENDLY.md # REQUIRED READING before adding/changing a skill
pyproject.toml tests/ .github/ # dev tooling, tests, CI/release (never shipped)
CHANGELOG.md # release history — what's shipped, per version
CLAUDE.md # grounding + build plan for a Claude Code session in this repo
An Azure AD app registration (public client, device-code / public-client flow enabled). Add
the delegated permissions Mail.Read + MailboxSettings.Read (read mail and list rules), plus
MailboxSettings.ReadWrite only if you want rule/category authoring, plus Mail.ReadWrite only if
you want to create search folders (--mode folders) or move messages (--mode messages). No cost, no admin consent for personal
accounts. Then export the resulting identifiers before first sign-in (read from the environment,
never hardcoded):
export MSGRAPH_CLIENT_ID="<application (client) id>"
export MSGRAPH_TENANT_ID="consumers" # or "common" for work/school + personal accountsSee plugin/skills/auth-login/SKILL.md for the full walkthrough.
python3 -m msgraph.client describe # discover every verb + schema
python3 -m msgraph.client auth-login # read-only sign-in (device code)
python3 -m msgraph.client mail-list --limit 10 # triage the inbox
python3 -m msgraph.client rule-verify --header_contains "List-Unsubscribe" # preview, read-only
python3 -m msgraph.client auth-login --mode rules # escalate (separate consent)
python3 -m msgraph.client rule-create --name "Newsletters" \
--header_contains "List-Unsubscribe" --move_to_folder "Newsletters"(Run from plugin/src/, or set PYTHONPATH=plugin/src. Inside an installed plugin the skills use
${CLAUDE_PLUGIN_ROOT}/src/msgraph/client.py.)
ruff and pytest are dev tooling only — they never ship inside plugin/. If python3 -m pip
is unavailable, install them isolated with uv
(uv tool install ruff pytest, then export PATH="$HOME/.local/bin:$PATH").
ruff check . && ruff format --check . # apply with `ruff format .` if it wants changes
python3 -m pytest -q
# Stdlib-only guard — the same denylist CI enforces in tests/test_stdlib_only.py. The shipped
# payload under plugin/src must import only the standard library (plus its own `msgraph` package).
grep -rnE '^\s*(import|from)\s+(msal|azure|requests|urllib3|httpx|aiohttp|pydantic|yaml|dotenv)\b' plugin/src && echo 'FORBIDDEN IMPORT' || echo 'stdlib-only OK'Built spec-first (/speckit-specify → clarify → plan → tasks → implement). As of v0.4.0:
the 14 verbs above (+ describe) across the four-tier scope ratchet, the runtime describe catalog,
and offline unit tests (Graph HTTP boundary mocked) — all behind a layered msgraph package (thin
client.py entrypoint over runtime/catalog/render/graph/verbs). Live auth/Graph behaviour
requires the one-time Azure app registration above. See CHANGELOG.md for the release history and
CLAUDE.md for the build plan.
MIT.