Local-first Microsoft 365 email, calendar, and Teams toolkit with MCP support and guarded write flows
Project description
ms365-toolkit
ms365-toolkit is a generic Microsoft 365 email, calendar, and Teams toolkit with:
- device-code auth
- Graph read clients and guarded email write flows
- local mailbox indexing
- triage and briefing workflows
- a local labeling workflow for improving per-user email intelligence
Public beta note: this is local-first tooling for delegated Microsoft 365 access. Users configure their own Microsoft Entra public-client app and grant the delegated Graph permissions required by the features they use.
See CHANGELOG.md for release notes.
Security and Privacy
- Tokens, local profile config, mailbox indexes, labels, audit logs, and usage analytics stay on the user's machine.
- Device-code auth uses a public-client Entra app registration; no client secret is required.
- Usage analytics record coarse command/tool metadata, durations, sizes, and sanitized errors, not email bodies, recipients, subjects, transcript text, raw Graph IDs, or tokens.
- Guarded email writes require a write token,
user.endpoint = "me", configured allowlists, rate limits, confirmation/audit checks, and a content hash for send. - Microsoft Graph access is delegated and tenant-controlled. Some Teams channel-message, file, transcript, and Copilot notes features require additional scopes, admin consent, licenses, or available tenant data.
Self-Service Setup
1. Prerequisites
- Python 3.11+
uv- A Microsoft 365 account with access to the mailbox you want to use
- A Microsoft Entra app registration configured as a public client for device-code auth
This project uses device-code flow. Users do not need to put a client secret in local config.
2. Create a Microsoft Entra App Registration
Create an app registration in your tenant and capture these two values:
tenant_idclient_id
Configure the app as a public client so device-code auth is allowed.
For normal read-only use, the delegated Microsoft Graph permissions should include:
Mail.ReadCalendars.ReadChat.ReadTeam.ReadBasic.AllChannel.ReadBasic.AllMailboxSettings.Read
If you want to read Teams channel messages, also grant:
ChannelMessage.Read.All
If you plan to add or use write operations later, also grant:
Mail.ReadWriteMail.SendCalendars.ReadWriteMailboxSettings.ReadWrite
If your organization requires admin consent for these scopes, an administrator must grant it before login will work.
Teams notes:
- Teams reads currently require
endpoint = "me". - Channel message reads typically require admin-approved consent for
ChannelMessage.Read.All. - Users who need channel-message reads should run
auth login --teams-channel-messages.
3. Create Local Config
Create a local profile config from the example:
mkdir -p ~/.ms365-toolkit/profiles/default
cp config.example.toml ~/.ms365-toolkit/profiles/default/config.toml
Edit ~/.ms365-toolkit/profiles/default/config.toml and set at least:
[auth]
tenant_id = "<your-entra-tenant-id>"
client_id = "<your-app-registration-client-id>"
[user]
endpoint = "me"
user_principal_name = ""
timezone = "America/Chicago"
Notes:
- Use
endpoint = "me"for your own mailbox. - Use
endpoint = "users"only for delegated/shared mailbox access, and then setuser_principal_nameto the target mailbox UPN. domain_allowlist = []is acceptable for read-only use, but write operations will be blocked until you set it.
4. Authenticate
Run device-code login:
uvx --from ms365-toolkit==0.1.15 ms365-toolkit auth login
If you need guarded email draft/send support:
uvx --from ms365-toolkit==0.1.15 ms365-toolkit auth login --write
If you need Teams channel-message reads:
uvx --from ms365-toolkit==0.1.15 ms365-toolkit auth login --teams-channel-messages
The CLI will print a verification URL and user code. Complete that sign-in flow in a browser.
5. Verify It Works
Check auth status:
uvx --from ms365-toolkit==0.1.15 ms365-toolkit auth status
Check write-token status:
uvx --from ms365-toolkit==0.1.15 ms365-toolkit auth status --write
Run tests:
uv run --with '.[dev]' pytest tests/unit/ -q
Try a basic mailbox read:
uvx --from ms365-toolkit==0.1.15 ms365-toolkit inbox --top 5
Try a basic Teams read:
uvx --from ms365-toolkit==0.1.15 ms365-toolkit list-chats --top 5
uvx --from ms365-toolkit==0.1.15 ms365-toolkit search-chats "Sara Kamal" --top 3
uvx --from ms365-toolkit==0.1.15 ms365-toolkit list-teams
uvx --from ms365-toolkit==0.1.15 ms365-toolkit search-teams-messages "cost center" --top 5
uvx --from ms365-toolkit==0.1.15 ms365-toolkit search-channel-messages <team_id> <channel_id> "authorization rate" --top 5
uvx --from ms365-toolkit==0.1.15 ms365-toolkit read-channel-thread <team_id> <channel_id> <message_id> --top 10
Global Teams search returns ranked search hits and snippets, not hydrated full message bodies.
Try attachment download:
uvx --from ms365-toolkit==0.1.15 ms365-toolkit download-email-attachments <message_id> --output-dir ./downloads
uvx --from ms365-toolkit==0.1.15 ms365-toolkit list-email-attachments <message_id>
uvx --from ms365-toolkit==0.1.15 ms365-toolkit read-email-attachment <message_id> <attachment_id> --max-chars 50000
Try the guarded email write flow:
uvx --from ms365-toolkit==0.1.15 ms365-toolkit create-email-draft --to user@example.com --subject "Hello" --body "<p>Hello</p>"
uvx --from ms365-toolkit==0.1.15 ms365-toolkit create-reply-draft <message_id> --body "<p>Reply</p>"
uvx --from ms365-toolkit==0.1.15 ms365-toolkit send-email-draft <draft_id> <content_hash>
Inspect local usage analytics:
uvx --from ms365-toolkit==0.1.15 ms365-toolkit usage summary --days 7
uvx --from ms365-toolkit==0.1.15 ms365-toolkit usage tools --days 7
uvx --from ms365-toolkit==0.1.15 ms365-toolkit usage errors --days 7
uvx --from ms365-toolkit==0.1.15 ms365-toolkit usage slow --days 7 --top 10
Usage analytics are local-only JSONL files under the active profile. They record coarse
tool/command metadata, durations, result sizes, and sanitized error categories. They do not
record email bodies, subjects, recipients, message IDs, transcript text, or tokens.
Use usage slow to find slow tools and oversized MCP responses.
Check whether a local CLI/MCP install is stale:
uvx --from ms365-toolkit==0.1.15 ms365-toolkit version --check
uvx --from ms365-toolkit==0.1.15 ms365-toolkit doctor
doctor reports the installed version, latest PyPI version, profile/config status,
local version-status cache path, and Codex/Claude update commands when stale.
5b. Installable MCP Setup
The public install shape is a local stdio MCP launched from the published wrapper package. These commands work after the corresponding package version has been published to PyPI.
Recommended auto-latest registration:
codex mcp add ms365 --env MS365_TOOLKIT_PROFILE=default --env UV_CACHE_DIR=/tmp/ms365-toolkit-uv-cache --env UV_TOOL_DIR=/tmp/ms365-toolkit-uv-tools -- sh -lc 'cd /tmp && exec uvx ms365-toolkit-mcp@latest'
claude mcp add -s user ms365 -e MS365_TOOLKIT_PROFILE=default -e UV_CACHE_DIR=/tmp/ms365-toolkit-uv-cache -e UV_TOOL_DIR=/tmp/ms365-toolkit-uv-tools -- sh -lc 'cd /tmp && exec uvx ms365-toolkit-mcp@latest'
The @latest suffix asks uvx to refresh cached package metadata and resolve the
latest published MCP wrapper when a new MCP process starts.
To update an existing pinned registration, rerun the recommended registration command for Codex and/or Claude. Then restart any active Codex or Claude sessions so they spawn a new MCP process. Verify the active registration with:
codex mcp get ms365
claude mcp get ms365
Both should show:
sh -lc cd /tmp && exec uvx ms365-toolkit-mcp@latest
Pinned/reproducible registration:
codex mcp add ms365 --env MS365_TOOLKIT_PROFILE=default --env UV_CACHE_DIR=/tmp/ms365-toolkit-uv-cache --env UV_TOOL_DIR=/tmp/ms365-toolkit-uv-tools -- sh -lc 'cd /tmp && exec uvx --from ms365-toolkit-mcp==0.1.15 ms365-toolkit-mcp'
claude mcp add -s user ms365 -e MS365_TOOLKIT_PROFILE=default -e UV_CACHE_DIR=/tmp/ms365-toolkit-uv-cache -e UV_TOOL_DIR=/tmp/ms365-toolkit-uv-tools -- sh -lc 'cd /tmp && exec uvx --from ms365-toolkit-mcp==0.1.15 ms365-toolkit-mcp'
Pinned registrations are deterministic but can become stale. Use them only when you need
an exactly reproducible MCP version. To inspect MCP version state in a session, call
get_toolkit_status or run ms365-toolkit doctor. Set MS365_TOOLKIT_WARN_STALE=1
to print a cache-only startup warning when a prior explicit check has already found a
newer version.
Email, Teams, and calendar list/search MCP tools return compact summaries by default and accept
max_chars to cap response size. Use the corresponding read/get tool when you need the
full message, event, thread, attachment, transcript, or meeting-notes payload.
When a compact MCP response returns truncated: true with next_offset, repeat the same
tool call with offset=<next_offset> to continue through the current ordered result set.
If next_offset is null, increase max_chars because the next item did not fit.
Graph-backed MCP collection tools can also return next_cursor. Repeat the same tool
with cursor=<next_cursor> to continue with Microsoft Graph's next page. If both
next_offset and next_cursor are possible, consume next_offset first; next_cursor
is only returned after the current response page fits inside max_chars.
Cursor support covers list_inbox, search_emails, list_events, search_events,
list_calendars, list_chats, list_chat_messages, search_teams_messages,
list_teams, list_channels, list_channel_messages, and list_channel_replies.
Local derived searches such as search_chats, search_chat_messages, and
search_channel_messages remain bounded first-page convenience filters. Use
search_teams_messages for paged global Teams message search.
Supported v1 tools include guarded email writes:
get_toolkit_statuslist_inboxsearch_emailsread_emaillist_email_attachmentsread_email_attachmentcreate_email_draftcreate_reply_draftsend_email_draftlist_folderslist_eventsget_eventsearch_eventslist_calendarsfind_free_slotslist_chatssearch_chatslist_chat_messagessearch_chat_messagessearch_teams_messagesread_chat_messagelist_teamslist_channelslist_channel_messagessearch_channel_messageslist_channel_repliesread_channel_messageread_channel_thread
All MCP datetime inputs must be timezone-aware ISO 8601 strings.
Email write operations require a write token, user.endpoint = "me", and a configured domain allowlist.
5c. Repo-Local MCP Fallback
Install the MCP extra:
uv sync --extra mcp
Run the local stdio MCP server:
uv run --extra mcp ms365-toolkit-mcp
Or run it directly from the module entrypoint:
uv run --extra mcp python -m ms365_toolkit.mcp
5d. Real MCP Smoke Test
Run the opt-in real-world MCP smoke test from the local checkout:
uv run --extra mcp python scripts/smoke_mcp.py --profile default
The smoke test launches the local stdio MCP server and calls read-only tools
against real Microsoft Graph data. Email, calendar, toolkit-status, compact
response, and continuation-shape checks are required. Teams, channel messages,
files, transcripts, and meeting notes are tiered optional checks and report
SKIP when tenant data or optional delegated scopes are unavailable.
The output is redacted by design. It prints only tool names, pass/fail/skip status, counts, booleans, and hashed ID prefixes. It does not print subjects, senders, recipients, message previews, event bodies, Teams message text, transcript text, meeting notes, tokens, or raw Graph IDs.
Useful variants:
uv run --extra mcp python scripts/smoke_mcp.py --profile work
uv run --extra mcp python scripts/smoke_mcp.py --timeout 60
5e. Repo-Local One-Command Setup
Use the installer script to wire everything up for both tools:
./scripts/setup_mcp.sh
This is the developer fallback, not the primary public install path.
It runs the MCP server from the local checkout and does not use the published
ms365-toolkit-mcp@latest package.
What it does:
- installs the MCP extra with
uv - checks that your selected MS365 profile is already authenticated
- registers the
ms365server with Codex - registers the
ms365server with Claude usinguserscope so it is available across sessions
Useful variants:
./scripts/setup_mcp.sh --target codex
./scripts/setup_mcp.sh --target claude
./scripts/setup_mcp.sh --profile default
./scripts/setup_mcp.sh --dry-run
Equivalent Make targets:
make mcp-setup
make mcp-setup-codex
make mcp-setup-claude
5f. Claude Review Workflow
This repo includes project-local Claude review commands and a shell workflow.
In an interactive claude session, use:
/review-critical
/review-critical teams changes
/review-commit f9c3079
/consult Should we expose participant names in 1:1 chats?
For terminal or Codex-driven runs, use:
./scripts/claude_workflow.sh review
./scripts/claude_workflow.sh review-commit f9c3079
./scripts/claude_workflow.sh consult "Should Teams support user.endpoint=users?"
Equivalent Make targets:
make claude-review
make claude-review-commit COMMIT=f9c3079
make claude-consult PROMPT="Should Teams support user.endpoint=users?"
6. Where Secrets and Local State Live
Do not put secrets or mailbox data into the repo.
Local config:
~/.ms365-toolkit/profiles/<profile>/config.toml
Token caches:
- stored in the OS keychain via
keyring - read cache key:
ms365-read-token-<profile> - write cache key:
ms365-write-token-<profile>
Mailbox-derived local state:
~/.ms365-toolkit/profiles/<profile>/mail_index.db~/.ms365-toolkit/profiles/<profile>/thread_labels.jsonl
Shareable Code vs Private Local State
The repository is intended to be generic and shareable.
Generic, shareable parts:
- source code under
src/ - tests under
tests/ - build and dependency config
- generic CLI commands and labeling UI
Private, per-user local state:
~/.ms365-toolkit/profiles/<profile>/config.toml- token caches
- local mailbox index databases
- exported label datasets such as
thread_labels.jsonl - any mailbox-derived training or evaluation data
Do not commit or share profile directories, token caches, mailbox indexes, or label datasets unless you explicitly want to share mailbox-derived data.
Local Workflow
- Configure a local profile under
~/.ms365-toolkit/profiles/<profile>/config.toml - Authenticate with:
uvx --from ms365-toolkit==0.1.15 ms365-toolkit auth login
- Build a local mailbox index:
uv run --with '.[dev]' ms365-toolkit sync-mail-index
- Export thread candidates for labeling:
uv run --with '.[dev]' ms365-toolkit export-label-candidates --top 30 --sync-index
- Review and label locally:
uv run --with '.[dev]' ms365-toolkit label-ui
The learned behavior from triage is driven by each user’s local labeled dataset, not by checked-in project state.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file ms365_toolkit-0.1.15.tar.gz.
File metadata
- Download URL: ms365_toolkit-0.1.15.tar.gz
- Upload date:
- Size: 268.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.7 {"installer":{"name":"uv","version":"0.11.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
937ee278a96ee6bbe79693ce3753d149c4a95fa477c107aacd115307981c35e1
|
|
| MD5 |
8a430efb7a415000ec3ca5421a8e26aa
|
|
| BLAKE2b-256 |
f84d49d436b3b7f03eee3e66f44a44a803ddd5a5d5da3859b01464f076690376
|
File details
Details for the file ms365_toolkit-0.1.15-py3-none-any.whl.
File metadata
- Download URL: ms365_toolkit-0.1.15-py3-none-any.whl
- Upload date:
- Size: 107.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.7 {"installer":{"name":"uv","version":"0.11.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
08a89fd90d838a5ce2030fe058428aadd1795f972e1d5870b0ea258afe4e54f7
|
|
| MD5 |
49b41ce5ce5253895e1ffc6b30f4a611
|
|
| BLAKE2b-256 |
f3405529148ffcf687e9493764d15bcf183bdf86b3820c8819ad54ee9720ecc9
|