Skip to content

Repository files navigation

msgraph-stdlib

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:

  1. Read Outlook mail — list/get messages and their headers, read-only.
  2. 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.
  3. Manage master categories — list and create-if-absent the coloured labels a rule assigns, so they always render with a colour.
  4. Create category search folders — virtual mailSearchFolder views over a category, behind a separate Mail.ReadWrite sign-in tier. A search folder never moves or deletes mail.
  5. Move messages — re-file one or more messages to a folder (message-move), behind a separate Mail.ReadWrite sign-in tier (--mode messages). MOVE-only and reversible, with a --dry_run preview; it never deletes (no delete-capable scope is ever requested).

Verbs (skills)

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).

Why stdlib / zero-backend

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.

Safety model (least privilege + verify-then-reversible)

  • 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. See docs/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 needs Mail.ReadWrite (--mode folders); moving messages needs Mail.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-move moves mail (reversible: move it back); it is the only per-message mutation, gated behind its own scope and a --dry_run preview.
  • 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 headerContains is coarse substring matching, so it must be checked against actual mail). message-move --dry_run applies 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.

Layout

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

Prerequisite (one-time, free)

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 accounts

See plugin/skills/auth-login/SKILL.md for the full walkthrough.

Quick start

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.)

Verify before done

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'

Status

Built spec-first (/speckit-specifyclarifyplantasksimplement). 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.

License

MIT.

About

No description, website, or topics provided.

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages