AI- and human-friendly CLI for Microsoft 365. Syncs calendars and contacts as local Markdown files and provides live access to mail, Teams, OneDrive, SharePoint, and Microsoft Loop components.
If you run an AI agent that needs access to your Microsoft 365 data, you're stuck with the Graph API: OAuth token management, pagination, rate limits, and multi-second roundtrips for every lookup. That's fine for write operations, but for reads — "when's my next meeting?" or "what's Jane's email?" — it's way too much overhead.
md365 syncs your M365 calendars and contacts to local Markdown files with YAML frontmatter. Your agent reads local files. Writes still go through the API.
# Sync once
md365 sync
# Then search however you want
rg "jane doe" ~/.local/share/md365/
grep -r "team sync" ~/.local/share/md365/*/calendar/
cat ~/.local/share/md365/work/contacts/jane-doe.mdNo tokens needed for reads. No API calls. Just files.
~/.local/share/md365/
├── work/
│ ├── calendar/
│ │ ├── 2026-02-24-team-sync.md
│ │ └── ...
│ └── contacts/
│ ├── jane-doe.md
│ └── ...
└── personal/
├── calendar/
└── contacts/
---
id: AAMkAGEx...
account: work
subject: Team Sync
start: 2026-02-24T16:00:00+01:00
end: 2026-02-24T18:00:00+01:00
location: https://zoom.us/j/123456
organizer: colleague@company.com
attendees:
- colleague@company.com
- you@company.com
response: accepted
online_meeting: true
last_modified: 2026-02-18T10:30:00Z
---
# Team Sync
Weekly team synchronization meeting.---
id: AAMkAGE4...
account: personal
display_name: Jane Doe
emails:
- jane@example.com
phones:
- "+49 123 456 789"
company: Acme Corp
job_title: Engineer
---
# Jane Doe
📧 jane@example.com
📱 +49 123 456 789
🏢 Acme Corp — Engineermd365 about # Explain the read model, cache, and account conventions
md365 commands --json # Inspect command/flag surface
md365 schema --json # Versioned command, permission, and safety contract
md365 skill install # Install the md365 agent skill
md365 --read-only --no-input mcp serve # Typed read-only MCP server over stdio
md365 mail list --account work --read-only
md365 auth status --no-input --json
md365 mail send --account work --to jane@example.com --subject "Hello" --body "..." --dry-run --json
md365 mail search "quarterly close" --account work --wrap-untrusted --sanitize-content --json
md365 sync # Sync all accounts
md365 sync --account work # Sync one account
md365 cal list # Upcoming events (14 days)
md365 cal list --from 2026-02-24 --to 2026-02-28
md365 cal list --search sync
md365 cal create --account work \ # Create event via API
--subject "Lunch" \
--start "2026-03-01T12:00" \
--end "2026-03-01T13:00"
md365 cal delete --account work --id <event-id>
md365 contacts search doe # Search local contacts
md365 mail list --account work # Recent mailbox messages
md365 mail list --account work --search bauer
md365 mail search "quarterly forecast" --account work
md365 mail search "project atlas" --account work --top-results
md365 mail list --account work --from-addr colleague@company.com --since 2026-05-01
md365 mail get --account work --id <message-id>
md365 mail attachments --account work --id <message-id>
md365 mail attachments --account work --id <message-id> --download ./attachments
md365 mail attachments --account work --id <message-id> --download ./attachments --name report.pdf
md365 teams list --account work
md365 teams channels --account work --team-id <team-id>
md365 teams files --account work --team-id <team-id> --channel-id <channel-id>
md365 onedrive list --account work
md365 onedrive list --account work --path "Projects/Current"
md365 onedrive list --account work --item-id <folder-item-id>
md365 files search "Jahresabschluss 2023" --account work
md365 loop list --account work
md365 loop search "project atlas" --account work
md365 sharepoint libraries --account work --team-id <team-id>
md365 sharepoint list --account work --team-id <team-id>
md365 sharepoint list --account work --site-id <site-id> --path "Projects/Current"
md365 sharepoint list --account work --drive-id <drive-id>
md365 mail send --account work \ # Send mail via API
--to "colleague@company.com" \
--subject "Hello" --body "Text"
md365 mail draft --account work \ # Create draft without sending
--to "colleague@company.com" \
--subject "Draft" --body "Text"
md365 auth login --account work # Device code OAuth login
md365 auth status # Token statussharepoint list --team-id and --site-id browse the default document
library. Use sharepoint libraries to discover additional libraries such as
Datenraum, then pass the returned ID to sharepoint list --drive-id.
files search searches all visible OneDrive and SharePoint content and returns
stable item, drive, and site IDs for follow-up commands.
loop search uses Microsoft Search to find current .loop and legacy .fluid
components visible to the signed-in account. With no query, its list/ls
aliases enumerate visible components. Results can include Loop content stored in
OneDrive, SharePoint, or indexed SharePoint Embedded storage. This is component
discovery, not a complete Loop workspace/container inventory: direct access to
Microsoft's Loop containers requires an app-specific SharePoint Embedded guest
permission grant.
mail search uses the Microsoft Search API across the signed-in user's own
Exchange Online mailbox, including supported attachment content. Results are
newest-first unless --top-results promotes Outlook's most relevant matches.
Use mail list --search instead for folder-aware chronological queries,
shared/delegated mailboxes, or Microsoft personal accounts.
Most commands support structured output:
md365 cal list --account work --json
md365 contacts search doe --ids-only
md365 mail list --account work --count
md365 mail search "quarterly close" --account work --results-only --select id,subject,from
md365 files search "annual report" --account work --json --fail-emptymd365 schema --json is the authoritative automation contract. It describes
aliases, positional arguments, flags/defaults, delegated Microsoft Graph
permissions, feature bundles, read/write mutability, prompting behavior,
per-command output modes, invocation constraints, and exit statuses. The compatibility policy is documented in
docs/command-schema.md.
Global execution policies are enforced before configuration, authentication, or command execution:
--read-onlyblocks every command catalogued as writing Microsoft Graph or local synchronized state; downloading attachments (mail attachments --download) is allowed because it performs only local writes, not mailbox mutations;--no-inputprevents browser, TUI, and Microsoft Entra authentication waits;--dry-runvalidates supported Outlook mail and calendar mutations and emits a redacted Graph operation preview without performing remote or local writes.
--read-only --dry-run is valid because the preview does not mutate state.
Unsupported dry runs fail closed with the stable policy_denied error code and
exit status 8.
Remote Exchange Online, Microsoft Teams, OneDrive, SharePoint, attachment, and
Microsoft Search text is preserved by default. For agent consumption, use
--wrap-untrusted to attach structured Microsoft 365 provenance and
--sanitize-content to make unsafe control/invisible Unicode characters
explicit. See docs/content-safety.md.
JSON success responses use a stable envelope with ok, data, optional
summary, meta, and breadcrumbs. Errors use ok: false, error, code,
optional hint, and http_status for Microsoft Graph responses. Collection
envelopes include count and has_more; complete local collections also
include total. Use --results-only to unwrap data, --select to project
fields, and --fail-empty to make an empty result exit with status 11. The
full output and exit-code contract is documented in
docs/automation-output.md.
md365 mcp serve provides nine typed, read-only tools for Exchange Online,
Outlook calendar, Microsoft Teams, OneDrive, SharePoint, Microsoft Loop, and Microsoft Search.
It uses stdio, starts without an authentication prompt, and exposes neither a
shell nor a generic Microsoft Graph request. Configure MCP hosts to launch it
with both safety guards:
{
"command": "md365",
"args": ["--read-only", "--no-input", "mcp", "serve"]
}Every remote Microsoft 365 result is wrapped as untrusted content with account
and resource provenance. See docs/mcp.md for the complete tool
surface, delegated permissions, and host configuration.
If you manage multiple accounts, md365 prevents you from accidentally sending mail or creating events from the wrong one. Configure associated domains per account:
accounts:
work:
domains:
- company.com
personal:
domains:
- gmail.comSending from personal to colleague@company.com will be blocked with a suggestion to use --account work. Override with --force.
Interactive setup (guided TUI):
md365 auth add -iOr non-interactive (AI-friendly):
md365 auth add --name work --hint you@company.com \
--scopes "Calendars.ReadWrite,Contacts.ReadWrite,User.Read,Mail.Read,Mail.Send" \
--domains "company.com" --loginInstead of assembling raw Microsoft Graph scopes, select the md365 commands or feature bundles you intend to use. md365 resolves them to the least-privilege scope set:
# Preview permissions without changing configuration or logging in
md365 auth plan --feature mail-manage --command "cal *"
# Configure a new account from feature bundles
md365 auth add --name work --hint you@company.com \
--feature mail-manage,mail-send,calendar,sync \
--domains "company.com" --login
# Incrementally request the permission needed by another command
md365 auth login --account work --command "mail send"
# Show token health and available/blocked commands for every account
md365 auth status
# Explain the same capability details for one account
md365 auth explain --account workAvailable feature bundles currently include mail-read, mail-manage,
mail-send, calendar-read, calendar, teams-read, files-read, loop-read, and
sync. Raw --scope/--scopes flags remain available as an expert escape
hatch.
md365 ships with a built-in public-client app registration — no Microsoft Entra setup is needed for personal use. A native-app client ID identifies the app; it is not a secret and does not grant access without user authorization and a token.
For an organization, prefer an account-specific, single-tenant Microsoft Entra
app registration. Enable public client flows, register a loopback redirect URI
such as http://localhost, add only the delegated Microsoft Graph permissions
shown by md365 auth plan, and apply the tenant's user/admin consent policy.
Set that registration's application (client) ID as client_id on the matching
md365 account and set tenant to the Microsoft Entra tenant ID or verified
domain. This limits app-identity, consent, and quota impact to the tenant;
it does not turn the client ID into a credential.
md365 auth login --account work
md365 syncMost tenants work with the default device code flow. If Microsoft Entra
Conditional Access blocks it, use the authorization code flow with S256 PKCE.
md365 also binds the loopback callback to the initiating browser flow with a
fresh OAuth 2.0 state value:
accounts:
work:
tenant: "contoso.onmicrosoft.com" # or the Microsoft Entra tenant GUID
client_id: "YOUR_SINGLE_TENANT_APPLICATION_CLIENT_ID"
auth_flow: authcode # opens browser instead of device code
hint: you@company.com
scope: "offline_access Calendars.ReadWrite User.Read"Config lives at ~/.config/md365/config.yaml:
accounts:
work:
hint: "you@company.com"
scope: "offline_access Calendars.ReadWrite Contacts.ReadWrite User.Read Mail.Read Mail.Send"
domains:
- company.com
personal:
hint: "you@outlook.com"
scope: "offline_access Calendars.ReadWrite Contacts.ReadWrite User.Read"
domains:
- gmail.comTokens are stored exclusively in the system keyring (gnome-keyring, macOS Keychain, Windows Credential Manager). A running keyring daemon is required — no file fallback.
The offline_access scope enables refresh tokens, so you only need to log in once per account. Tokens refresh automatically on use and remain valid for up to 90 days of inactivity.
brew install lcorneliussen/md365/md365yay -S md365-bingo install github.com/lcorneliussen/md365@latestDownload pre-built binaries for Linux, macOS, and Windows from Releases.
git clone https://github.com/lcorneliussen/md365.git
cd md365
go build -o md365 .- Events: Full window sync (past 30 → future 90 days). Remotely deleted events are removed locally.
- Contacts: Delta sync via Graph API for incremental updates.
- Direction: One-way (remote → local). Local files are a read-only cache.
MIT