msgraph-axi (Microsoft Graph axi)

September 13, 2026 · View on GitHub

ci npm license node

msgraph-axi header

Agent-ergonomic TOON wrapper for Microsoft Graph — Outlook mail and calendar first, with a raw Graph bridge for everything else. Built on the CLI for Microsoft 365 (m365) as a single backend, following the AXI design principles.

Why AXI: CLI vs MCP vs AXI

Across extensive AXI benchmark studies (over 900 runs), agent-first CLIs achieve 100% task success at ~50% fewer turns and 50–66% lower cost than MCP:

InterfaceContext (Turn 0)OutputMeasured PayloadWrite SafetyGuidance
msgraph-axi~55 tokens (on-demand)TOON-62.0% avg (up to -77.8% on mail listings)Dry-run by default; --execute / --confirmStructured hints (help[])
Raw CLI (m365)~0 tokensJSON / ASCIIBaseline (huge Graph OData payloads)❌ Direct mutationsHuman text / exit codes
Graph MCP~25k–35k tokens (100+ schemas)JSON-RPCHighest overhead (full schemas resent every turn)VariesSchema validation errors

Install & Setup

Install globally (recommended for repeated use):

npm install -g @jeffreyhaen/msgraph-axi @pnp/cli-microsoft365

For a one-off invocation without installing:

npx -y @jeffreyhaen/msgraph-axi --help

Agent integration

Install the skill globally so an agent loads the usage guide on demand:

npx skills add jeffreyhaen/msgraph-axi --skill msgraph-axi -g

Omit -g to install the skill for the current project only.

Initial Entra ID / M365 Setup

The underlying CLI for Microsoft 365 requires an Entra ID application registration in your tenant:

  1. Automatic setup (quickest): Run m365 setup and choose Create a new app registration. It registers an app with the CLI presets and stores the App ID in your local config.

  2. Existing or manual App Registration: If your organization already has an Entra app registered for CLI / developer use (or you create one manually):

    • Set redirect URI (Public client/mobile & desktop): https://login.microsoftonline.com/common/oauth2/nativeclient
    • Grant Delegated Microsoft Graph permissions: Mail.ReadWrite, Mail.Send, Calendars.ReadWrite, User.Read (optional: MailboxSettings.Read so calendar --timezone can default to your mailbox time zone instead of this machine's)
    • Save the App ID and Tenant in your CLI config:
      m365 cli config set --key clientId --value "<your-app-id>"
      m365 cli config set --key tenantId --value "<your-tenant-id>"
      

Then log in:

msgraph-axi auth login
msgraph-axi          # dashboard

See docs/authentication.md for headless / CI flows and details.

Commands

CommandPurpose
auth status|login|logoutm365 sign-in state; device-code and headless logins (see Authentication)
mail list [--folder] [--start] [--end]messages with compact defaults
mail read <id> [--full]snippet by default, body with --full
mail send ... [--execute]dry-run without --execute; --execute saves a draft, --send --execute delivers it
mail send --draft <id> [--execute]deliver a saved draft
mail delete <id> [--execute --confirm <id>]destructive, double-gated
mail search --search "..."Graph message search
mail draft --to ... [--execute]save a draft, review, then mail send --draft
mail thread <conversationId>the whole conversation, oldest first
mail attachment get <id> --message <id> [--out <path>]download an attachment
calendar listcalendars of the signed-in (or --user) account
calendar agenda [--start] [--end] [--calendar]events, default today..+7d
calendar create | update [--execute]events via the Graph REST bridge; --show-as free|tentative|busy|oof|workingElsewhere
calendar cancel | delete [--execute --confirm <id>]destructive, double-gated
calendar availability --schedules a@x.comfree/busy via getSchedule, availabilityView plus a code legend
calendar suggest --attendees a@x.com --duration 60meeting slot search via findMeetingTimes; confirm a slot with availability
user get <upn>profile plus manager lookup
user search <term>directory lookup by name, mail or upn prefix
raw <path> [--method] [--body] [--execute]any Graph endpoint through m365 request

Conventions

  • TOON on stdout, structured { error, code, help[] } errors on stdout.
  • Exit codes: 0 success (incl. no-ops), 1 runtime error, 2 usage error.
  • Unknown flags rejected by name; lists take --limit / --fields a,b; detail views truncate at 200 chars and lift with --full.
  • Read-only by default; writes require --execute, destructive writes also --confirm <id>.
  • Mail is never delivered by --execute alone: mail send --execute saves a draft and --send --execute delivers it, so a wrong recipient or body stays fixable.
  • Long or multi-line payloads travel as JSON (through a temp file when needed): on Windows the backend is spawned via cmd.exe, which silently cuts an argument at a line break, so the CLI refuses such an argument instead of sending corrupt data.
  • Calendar reads and writes work in your mailbox time zone by default (Graph mailboxSettings, then this machine's zone, then UTC); pass --timezone <tz> to override. --start/--end accept 2026-03-15, a wall clock in that zone (2026-03-15T13:00:00) or an offset (2026-03-15T13:00:00+02:00).

Development

pnpm install
pnpm test          # vitest against a fake m365 fixture
pnpm typecheck
pnpm build
pnpm dev -- mail list   # run against a real m365 login

Tests run against test/fixtures/m365-cli.js — no Microsoft 365 tenant required. Live smoke tests need m365 login; the fixture can be pointed at with MSGRAPH_AXI_M365_BIN.

Releasing

  1. Move the Unreleased section in CHANGELOG.md under ## [x.y.z] - <date>.
  2. Bump version in package.json and VERSION in src/version.ts.
  3. Commit as chore: release vX.Y.Z, then tag and push:
git tag -a vX.Y.Z -m "Release vX.Y.Z"
git push --follow-tags

Pushing the tag runs .github/workflows/release.yml: it builds, tests, publishes to npm with provenance, and creates the GitHub release with the matching CHANGELOG.md section as its body (scripts/release-notes.mjs). An existing release is left as is, so the workflow can be re-run safely.

See also