Metadata-Version: 2.4
Name: ms365-toolkit
Version: 0.2.2
Summary: Local-first Microsoft 365 email, calendar, and Teams toolkit with MCP support and guarded write flows
Project-URL: Homepage, https://github.com/sadhiappan/ms365-toolkit
Project-URL: Repository, https://github.com/sadhiappan/ms365-toolkit
Project-URL: Issues, https://github.com/sadhiappan/ms365-toolkit/issues
Author-email: Shiv Adhiappan <shivaram1190@gmail.com>
Maintainer-email: Shiv Adhiappan <shivaram1190@gmail.com>
License-Expression: MIT
License-File: LICENSE
Keywords: calendar,email,mcp,microsoft-365,microsoft-graph,teams
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Communications :: Email
Classifier: Topic :: Office/Business
Requires-Python: >=3.11
Requires-Dist: beautifulsoup4<5,>=4.12
Requires-Dist: idna<4,>=3.15
Requires-Dist: keyring<26,>=25.0
Requires-Dist: msal<2,>=1.31
Requires-Dist: openpyxl<4,>=3.1
Requires-Dist: packaging<26,>=24
Requires-Dist: pyjwt<3,>=2.8
Requires-Dist: pypdf<6,>=5.0
Requires-Dist: python-docx<2,>=1.1
Requires-Dist: python-pptx<2,>=1.0
Requires-Dist: requests<3,>=2.33
Requires-Dist: structlog<26,>=24.4
Requires-Dist: urllib3<3,>=2.7
Provides-Extra: dev
Requires-Dist: mypy<2,>=1.13; extra == 'dev'
Requires-Dist: pytest-asyncio<1,>=0.24; extra == 'dev'
Requires-Dist: pytest-cov<7,>=6.0; extra == 'dev'
Requires-Dist: pytest<9,>=8.3; extra == 'dev'
Requires-Dist: ruff<1,>=0.8; extra == 'dev'
Requires-Dist: types-openpyxl<4,>=3.1; extra == 'dev'
Provides-Extra: mcp
Requires-Dist: fastmcp<4,>=3.0; extra == 'mcp'
Requires-Dist: pygments<3,>=2.20; extra == 'mcp'
Description-Content-Type: text/markdown

# ms365-toolkit

`ms365-toolkit` is a local-first Microsoft 365 email, calendar, and Teams toolkit for AI agents and maintainers. It provides:
- 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.
Each installation uses its own Microsoft Entra public-client app and grants the
delegated Graph permissions required by the features in use.

See [CHANGELOG.md](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.
- Guarded calendar writes support own and delegated/shared calendars, record the target endpoint in local audit logs, and require confirmation plus content-hash checks for mutations.
- MCP share-link downloads are confined to the active profile's downloads directory; use the CLI for explicit arbitrary local output paths.
- 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.

## Agent Setup

Use this path when an AI agent needs to operate Microsoft 365 through Codex or
Claude. A repository clone is not required for normal MCP use.

### Runtime Model

- The AI client launches a local stdio MCP server named `ms365`.
- The MCP server runs on the local machine through `uvx --refresh-package ms365-toolkit-mcp ms365-toolkit-mcp@latest`.
- The MCP server uses your Microsoft 365 delegated login to call Microsoft Graph.
- Tokens are stored in your OS keychain and profile config stays under `~/.ms365-toolkit`.
- No client secret is used or stored.

### Before You Start

Required before registration:

- Python 3.11 or newer.
- `uv`, verified with `uv --version`.
- Codex CLI or Claude CLI, verified with `codex --version` or `claude --version`.
- A Microsoft 365 work or school account with access to the mailbox, calendar, and Teams data the agent should operate on.
- Permission to create or update a Microsoft Entra app registration. If you cannot access app registrations or admin consent, ask your Microsoft 365 tenant admin.

Install `uv` if it is missing:

```bash
curl -LsSf https://astral.sh/uv/install.sh | sh
```

### 1. Create the Microsoft Entra App

Open the Microsoft Entra admin center:

```text
https://entra.microsoft.com
```

Create the app registration:

- Go to `Identity` -> `Applications` -> `App registrations` -> `New registration`.
- Name it something recognizable, such as `ms365-toolkit-local`.
- Choose `Accounts in this organizational directory only` unless you intentionally need a multi-tenant app.
- Leave redirect URI blank.
- Select `Register`.

Copy these two values from the app `Overview` page:

- `Application (client) ID`: this becomes `client_id`.
- `Directory (tenant) ID`: this becomes `tenant_id`.

These are IDs, not passwords or API keys.

Enable device-code login:

- Go to `Authentication`.
- Under `Advanced settings`, set `Allow public client flows` to `Yes`.
- Select `Save`.

Do not create a client secret for this toolkit. Device-code auth uses a public
client app registration. No client secret is required.

Microsoft references:

- App registration: https://learn.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app
- Public client flows: https://learn.microsoft.com/en-us/entra/identity-platform/msal-client-applications

### 2. Add Microsoft Graph Permissions

In the app registration:

- Go to `API permissions`.
- Select `Add a permission`.
- Choose `Microsoft Graph`.
- Choose `Delegated permissions`.
- Add the permissions for the features you want.
- If your tenant requires admin consent, have an admin select `Grant admin consent`.

Start with the base read-only permissions:

| Feature | Delegated permissions | Login command |
| --- | --- | --- |
| Email, calendar, basic Teams reads | `Mail.Read`, `Calendars.Read`, `MailboxSettings.Read`, `Chat.Read`, `Team.ReadBasic.All`, `Channel.ReadBasic.All` | `auth login` |
| Teams channel messages | `ChannelMessage.Read.All` | `auth login --teams-channel-messages` |
| Teams file downloads | `Files.Read` | `auth login --teams-files` |
| Email inline SharePoint/OneDrive link downloads | `Files.Read.All`, `Sites.Read.All` | `auth login --share-links` |
| Meeting transcripts | `OnlineMeetings.Read`, `OnlineMeetingTranscript.Read.All` | `auth login --meeting-transcripts` |
| Meeting notes and Copilot insights | `OnlineMeetings.Read`, `OnlineMeetingAiInsight.Read.All` | `auth login --meeting-notes` |
| Directory org-tree reads | `User.Read.All` | `auth login --directory` |
| Guarded email and calendar writes | `Mail.ReadWrite`, `Mail.Send`, `Calendars.ReadWrite`, `MailboxSettings.ReadWrite` | `auth login --write` |

Only add optional permissions for features you plan to use. Channel messages,
meeting transcripts, meeting notes, directory org-tree reads, inline SharePoint/OneDrive link downloads,
and some tenant-wide Teams data commonly require admin consent, tenant policy
support, the right license, and actual available data.

Microsoft references:

- Microsoft Graph permissions: https://learn.microsoft.com/en-us/graph/permissions-reference
- Admin consent behavior: https://learn.microsoft.com/en-us/entra/identity-platform/consent-types-developer

### 3. Create Local Config

Create the default profile config:

```bash
mkdir -p ~/.ms365-toolkit/profiles/default
cat > ~/.ms365-toolkit/profiles/default/config.toml <<'EOF'
[auth]
tenant_id = "<paste-directory-tenant-id-here>"
client_id = "<paste-application-client-id-here>"

[user]
endpoint = "me"
user_principal_name = ""
timezone = "America/Chicago"

[safety]
domain_allowlist = []
folder_allowlist = ["Archive", "Follow Up"]
send_email_per_hour = 5
send_email_per_day = 20
draft_email_per_hour = 10
draft_email_per_day = 30
reply_email_per_hour = 5
reply_email_per_day = 20
forward_email_per_hour = 5
forward_email_per_day = 20
move_email_per_hour = 20
flag_email_per_hour = 50
create_event_per_hour = 5
create_event_per_day = 20
update_event_per_hour = 10
respond_event_per_hour = 10

[vip]
emails = []
domains = []

[intelligence]
critical_keywords = ["exec", "leadership", "decision", "board"]
noise_keywords = ["ooo", "focus", "block"]
action_keywords = ["urgent", "P1", "approve", "deadline"]
EOF
```

Edit the file and replace:

- `<paste-directory-tenant-id-here>` with the `Directory (tenant) ID`.
- `<paste-application-client-id-here>` with the `Application (client) ID`.
- `timezone` with your timezone if `America/Chicago` is not correct.

Keep `endpoint = "me"` for normal personal mailbox, calendar, and Teams access.
`endpoint = "users"` is only for delegated/shared mailbox scenarios and requires
`user_principal_name`.

`domain_allowlist = []` is safe for read-only use. Guarded email writes will stay
blocked until you explicitly add allowed recipient domains.

### 4. Authenticate

Run the base read-only login:

```bash
uvx --from ms365-toolkit@latest ms365-toolkit auth login
```

The CLI prints a verification URL and user code. Open the URL in a browser,
enter the code, and sign in with your Microsoft 365 account.

Run one login command with the optional flags you configured. For example, you
can combine flags:

```bash
uvx --from ms365-toolkit@latest ms365-toolkit auth login --teams-channel-messages --teams-files --share-links --meeting-transcripts --meeting-notes --directory
uvx --from ms365-toolkit@latest ms365-toolkit auth login --write
```

Use `--write` separately because write tokens are stored separately from read
tokens.

Check status:

```bash
uvx --from ms365-toolkit@latest ms365-toolkit auth status
uvx --from ms365-toolkit@latest ms365-toolkit doctor
```

### 5. Add the MCP to Codex or Claude

Recommended Codex registration:

```bash
codex mcp add ms365 --env MS365_TOOLKIT_PROFILE=default --env MS365_TOOLKIT_CLIENT=codex --env MS365_TOOLKIT_WARN_STALE=1 --env UV_CACHE_DIR=/tmp/ms365-toolkit-uv-cache --env UV_TOOL_DIR=/tmp/ms365-toolkit-uv-tools -- sh -lc 'cd /tmp && exec uvx --refresh-package ms365-toolkit-mcp ms365-toolkit-mcp@latest'
```

Recommended Claude registration:

```bash
claude mcp add -s user ms365 -e MS365_TOOLKIT_PROFILE=default -e MS365_TOOLKIT_CLIENT=claude -e MS365_TOOLKIT_WARN_STALE=1 -e UV_CACHE_DIR=/tmp/ms365-toolkit-uv-cache -e UV_TOOL_DIR=/tmp/ms365-toolkit-uv-tools -- sh -lc 'cd /tmp && exec uvx --refresh-package ms365-toolkit-mcp ms365-toolkit-mcp@latest'
```

Restart any open Codex or Claude sessions after registration. Existing sessions
usually keep using the MCP processes they started with.

Verify the saved registration:

```bash
codex mcp get ms365
claude mcp get ms365
```

Both should show a command ending with:

```bash
uvx --refresh-package ms365-toolkit-mcp ms365-toolkit-mcp@latest
```

The `--refresh-package` flag refreshes cached package metadata, and the
`@latest` suffix resolves the latest published MCP wrapper when a new MCP
process starts. The `MS365_TOOLKIT_WARN_STALE=1` environment variable enables
startup and response warnings if a process is still running an older version.

### 6. Verify Inside Your AI Session

After restarting Codex or Claude, ask the AI to run these MCP checks:

```text
Use the ms365 MCP tool get_toolkit_status with include_latest=true.
Use the ms365 MCP tool list_inbox with top 3 and max_chars 12000.
```

A healthy setup should return toolkit status and a small inbox summary. If the
tool is missing, the AI session probably started before MCP registration or is
not using the profile where `ms365` was added.

You can also verify from a terminal:

```bash
UV_CACHE_DIR=/tmp/ms365-toolkit-uv-cache uvx --from ms365-toolkit@latest ms365-toolkit inbox --top 5
UV_CACHE_DIR=/tmp/ms365-toolkit-uv-cache uvx --from ms365-toolkit@latest ms365-toolkit version --check
```

### Example Use Cases

After setup, ask Codex or Claude to use the `ms365` MCP with prompts like these.

#### Check Toolkit Health

```text
Use the ms365 MCP tool get_toolkit_status with include_latest=true and tell me whether the toolkit is connected, current, and ready to use.
```

Needs: base setup.

#### Triage Recent Inbox

```text
Use ms365 to list my latest inbox messages, then summarize the top actionable items, deadlines, and who I need to reply to.
```

Needs: base read-only login.

#### Understand an Email Thread

```text
Search my email for "Q3 Vendor Planning", read the conversation thread, and tell me what is going on, open decisions, blockers, and next actions.
```

Needs: base read-only login.

#### Prepare for a Meeting

```text
List my calendar for tomorrow, identify the highest-priority meeting, then search related email and Teams context to create a short prep brief.
```

Needs: base read-only login. Teams context requires the relevant Teams permissions.

#### Pull a Meeting Transcript

```text
Find yesterday's "Pricing Review Knowledge Transfer" meeting and pull the transcript if it was recorded and transcribed. Summarize decisions, risks, and action items.
```

Needs: `auth login --meeting-transcripts`.

#### Pull Meeting Notes

```text
Find the meeting notes or Copilot insights for my latest project meeting and summarize key decisions and follow-ups.
```

Needs: `auth login --meeting-notes`.

#### Search Teams Context

```text
Search Teams messages for "forecast model" and summarize the relevant threads, who said what, and what needs follow-up.
```

Needs: base Teams read permissions. Channel messages require
`auth login --teams-channel-messages`.

#### Read Teams-Shared Files

```text
Find files attached to this Teams message or thread, read the supported documents, and summarize what they contain.
```

Needs: `auth login --teams-files`.

#### Download Files Linked Inside Email

```text
Read this email, list any SharePoint or OneDrive links embedded in the body, download the linked documents, and summarize the extracted text.
```

CLI equivalent:

```bash
ms365-toolkit download-email-share-links MESSAGE_ID --output-dir ./downloads
```

Needs: `auth login --share-links`.

#### Draft a Guarded Reply

```text
Draft a reply to this email thread that confirms the next steps. Do not send it. Create a draft only and show me the content hash.
```

Needs: `auth login --write` and `domain_allowlist` configured.

#### Find Availability

```text
Find 30-minute windows this week when these attendees are free: person1@example.com and person2@example.com.
```

Needs: base calendar read permissions.

### Common Problems

- `config.toml not found`: rerun the config creation command and confirm the file exists at `~/.ms365-toolkit/profiles/default/config.toml`.
- `invalid_client` or login says the client cannot be found: check that `client_id` is the `Application (client) ID` and `tenant_id` is the `Directory (tenant) ID`.
- Device-code login is blocked: confirm `Authentication` -> `Advanced settings` -> `Allow public client flows` is set to `Yes`.
- Permission or consent error: add the missing Microsoft Graph delegated permission and ask a tenant admin to grant admin consent if required.
- Teams channel messages fail: confirm `ChannelMessage.Read.All` was added and login was run with `--teams-channel-messages`.
- Teams file downloads fail: confirm `Files.Read` was added and login was run with `--teams-files`.
- Email-linked SharePoint/OneDrive downloads fail: confirm `Files.Read.All` and `Sites.Read.All` were added and login was run with `--share-links`.
- Meeting transcripts are empty or skipped: the meeting must have been recorded/transcribed, the signed-in user must have access, and login must include `--meeting-transcripts`.
- Meeting notes are empty or skipped: Copilot notes must exist, the signed-in user must have access, and login must include `--meeting-notes`.
- MCP is not visible in Codex or Claude: run `codex mcp get ms365` or `claude mcp get ms365`, then restart the AI session.
- MCP version is stale: use the `@latest` registration command again, restart the AI session, and run `get_toolkit_status`.
- Email writes are blocked: run `auth login --write`, keep `endpoint = "me"`, and add allowed recipient domains to `domain_allowlist`.

## MCP Tool Behavior

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.
Email `list_inbox` and `search_emails` return short `message_ref` values by
default so agents can call `read_email`, attachment, share-link, and reply tools
without copying long Graph IDs. Pass `include_ids=true` only when you need raw
message IDs.
`read_email` already returns body share links. Use `inspect_email_assets` when
you need the email body, downloadable attachments, and body share links together
without separate MCP calls.

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_status`
- `get_capability_status`
- `list_inbox`
- `search_emails`
- `read_email`
- `inspect_email_assets`
- `read_conversation_thread`
- `get_org_tree`
- `list_email_attachments`
- `read_email_attachment`
- `list_email_share_links`
- `download_share_link`
- `download_email_share_links`
- `create_email_draft`
- `create_reply_draft`
- `create_reply_all_draft`
- `send_email_draft`
- `discard_draft`
- `flag_email`
- `unflag_email`
- `move_email`
- `archive_email`
- `list_folders`
- `list_events`
- `get_event`
- `search_events`
- `list_calendars`
- `find_free_slots`
- `create_event`
- `update_event`
- `cancel_event`
- `respond_event`
- `list_chats`
- `search_chats`
- `list_chat_messages`
- `search_chat_messages`
- `search_teams_messages`
- `read_chat_message`
- `list_teams`
- `list_channels`
- `list_channel_messages`
- `search_channel_messages`
- `list_channel_replies`
- `read_channel_message`
- `read_channel_thread`
- `list_teams_message_files`
- `read_teams_message_file`
- `list_meeting_transcripts`
- `read_meeting_transcript`
- `read_meeting_notes`

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. Calendar write operations support both `endpoint = "me"` and
delegated/shared `endpoint = "users"` calendars when the profile has write
scopes. Event create checks new attendees against `domain_allowlist`; event
update/cancel/respond require the current `content_hash` from `get_event` or
`list_events` before mutating the calendar. Move/archive actions also require
the destination folder to match `folder_allowlist`.
Use `create_email_draft` for standalone new messages, `create_reply_draft` for
sender-only threaded replies, and `create_reply_all_draft` for threaded
reply-all drafts. Reply drafts are created through Graph reply actions and
reported with `thread_preservation_status`, `source_internet_message_id`,
`source_conversation_id`, `draft_in_reply_to`, and `draft_references` metadata
so callers can verify true thread linkage. Use `quote_mode="preserve"` for real
replies; `quote_mode="replace"` is only for intentionally non-thread-preserving
drafts and requires `preserve_thread=false` (CLI: `--no-preserve-thread`).
Draft results warn when a subject starts with `RE:` but the draft will not send
as a preserved-thread reply. Reply and reply-all drafts may include external
recipients who were already original thread participants; newly added external
recipients are still blocked unless they match `domain_allowlist`. Use
`discard_draft` / `discard-draft` to clean up a bad draft by draft ID after the
tool verifies the target is still a draft. Use `flag_email`, `unflag_email`,
`move_email`, and `archive_email` for guarded mailbox state changes.

## CLI Verification and Useful Commands

Try a basic Teams read:

```bash
uvx --from ms365-toolkit@latest ms365-toolkit list-chats --top 5
uvx --from ms365-toolkit@latest ms365-toolkit search-chats "Alex Rivera" --top 3
uvx --from ms365-toolkit@latest ms365-toolkit list-teams
uvx --from ms365-toolkit@latest ms365-toolkit search-teams-messages "budget owner" --top 5
uvx --from ms365-toolkit@latest ms365-toolkit search-channel-messages <team_id> <channel_id> "approval rate" --top 5
uvx --from ms365-toolkit@latest 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:

```bash
uvx --from ms365-toolkit@latest ms365-toolkit download-email-attachments <message_id> --output-dir ./downloads
uvx --from ms365-toolkit@latest ms365-toolkit list-email-attachments <message_id>
uvx --from ms365-toolkit@latest ms365-toolkit read-email-attachment <message_id> <attachment_id> --max-chars 50000
uvx --from ms365-toolkit@latest ms365-toolkit list-email-share-links <message_id>
uvx --from ms365-toolkit@latest ms365-toolkit download-share-link "https://contoso.sharepoint.com/sites/example/Shared%20Documents/brief.docx" --output-dir ./downloads
```

Meeting transcript reads preserve VTT cue timestamps in MCP `segments` and print
timestamped speaker blocks in the CLI while keeping flattened `text_content` for
compatibility.

Inspect a directory reporting tree:

```bash
uvx --from ms365-toolkit@latest ms365-toolkit org-tree "person@example.com"
uvx --from ms365-toolkit@latest ms365-toolkit org-tree "Alex Rivera" --report-depth 2
```

Try the guarded email write flow:

```bash
uvx --from ms365-toolkit@latest ms365-toolkit create-email-draft --to user@example.com --subject "Hello" --body "<p>Hello</p>"
uvx --from ms365-toolkit@latest ms365-toolkit create-reply-draft <message_id> --body "<p>Reply</p>"
uvx --from ms365-toolkit@latest ms365-toolkit create-reply-all-draft <message_id> --body "<p>Reply all</p>"
uvx --from ms365-toolkit@latest ms365-toolkit create-reply-draft <message_id> --body "<p>New body only</p>" --quote-mode replace --no-preserve-thread
uvx --from ms365-toolkit@latest ms365-toolkit send-email-draft <draft_id> <content_hash>
uvx --from ms365-toolkit@latest ms365-toolkit discard-draft <draft_id>
uvx --from ms365-toolkit@latest ms365-toolkit flag-email <message_id>
uvx --from ms365-toolkit@latest ms365-toolkit unflag-email <message_id>
uvx --from ms365-toolkit@latest ms365-toolkit move-email <message_id> <folder_id>
uvx --from ms365-toolkit@latest ms365-toolkit archive-email <message_id>
```

Try the guarded EA calendar write flow:

```bash
uvx --from ms365-toolkit@latest ms365-toolkit create-event --subject "Planning" --start 2026-04-03T09:00:00-05:00 --end 2026-04-03T09:30:00-05:00 --required-attendees person@example.com --online-meeting
uvx --from ms365-toolkit@latest ms365-toolkit update-event <event_id> <content_hash> --start 2026-04-03T10:00:00-05:00 --end 2026-04-03T10:30:00-05:00
uvx --from ms365-toolkit@latest ms365-toolkit respond-event <event_id> <content_hash> accept --comment "Confirmed"
uvx --from ms365-toolkit@latest ms365-toolkit cancel-event <event_id> <content_hash> --comment "Rescheduling"
```

Inspect local usage analytics:

```bash
uvx --from ms365-toolkit@latest ms365-toolkit usage summary --days 7
uvx --from ms365-toolkit@latest ms365-toolkit usage tools --days 7
uvx --from ms365-toolkit@latest ms365-toolkit usage clients --days 7
uvx --from ms365-toolkit@latest ms365-toolkit usage sessions --days 7 --top 10
uvx --from ms365-toolkit@latest ms365-toolkit usage errors --days 7
uvx --from ms365-toolkit@latest ms365-toolkit usage slow --days 7 --top 10
uvx --from ms365-toolkit@latest ms365-toolkit usage health --days 7 --top 5
uvx --from ms365-toolkit@latest ms365-toolkit usage health --version current
uvx --from ms365-toolkit@latest ms365-toolkit usage summary --client codex --surface mcp
```

Usage analytics are local-only JSONL files under the active profile. They record
coarse tool/command metadata, client/session/request IDs, sanitized input shape
metrics, durations, result sizes, sanitized Graph endpoint templates, retry
counts, and sanitized error categories. They do not record email
bodies, subjects, recipients, message IDs, full URLs, query strings, transcript
text, raw Graph IDs, or tokens. Use `usage health` to identify failure hotspots,
slow tools, large responses, Graph endpoint/status hotspots, and recommended
fixes. Invalid-input failures are grouped with stable sanitized error codes
instead of raw user-provided values.
By default, `usage health` focuses on the current toolkit version when current
events exist; use `--scope all` to include older versions. Usage reports also
support `--version`, `--client`, `--surface`, and `--name` filters.

Inspect currently running local MCP processes from a checkout:

```bash
uv run python scripts/inspect_mcp_sessions.py
```

Pinned/reproducible MCP registration:

```bash
codex mcp add ms365 --env MS365_TOOLKIT_PROFILE=default --env MS365_TOOLKIT_CLIENT=codex --env MS365_TOOLKIT_WARN_STALE=1 --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.2.2 ms365-toolkit-mcp'
claude mcp add -s user ms365 -e MS365_TOOLKIT_PROFILE=default -e MS365_TOOLKIT_CLIENT=claude -e MS365_TOOLKIT_WARN_STALE=1 -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.2.2 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`.
`MS365_TOOLKIT_WARN_STALE=1` performs a short live version check when the MCP
process starts, prints a startup warning if stale, caches the result, and adds a
`version_warning` field to MCP tool responses while the cached status is stale.

## Developer Workflow

### Repo-Local MCP Fallback

Install the MCP extra:

```bash
uv sync --extra mcp
```

Run the local stdio MCP server:

```bash
uv run --extra mcp ms365-toolkit-mcp
```

Or run it directly from the module entrypoint:

```bash
uv run --extra mcp python -m ms365_toolkit.mcp
```

### Real MCP Smoke Test

Run the opt-in real-world MCP smoke test from the local checkout:

```bash
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:

```bash
uv run --extra mcp python scripts/smoke_mcp.py --profile work
uv run --extra mcp python scripts/smoke_mcp.py --timeout 60
```

### Repo-Local One-Command Setup

Use the installer script to wire everything up for both tools:

```bash
./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 `ms365` server with Codex
- registers the `ms365` server with Claude using `user` scope so it is available across sessions
- enables stale-version warnings for both registered MCP sessions

Useful variants:

```bash
./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:

```bash
make mcp-setup
make mcp-setup-codex
make mcp-setup-claude
```

Run unit tests:

```bash
uv run --with '.[dev]' pytest tests/unit/ -q
```

### Claude Review Workflow

This repo includes project-local Claude review commands and a shell workflow.

In an interactive `claude` session, use:

```text
/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:

```bash
./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:

```bash
make claude-review
make claude-review-commit COMMIT=f9c3079
make claude-consult PROMPT="Should Teams support user.endpoint=users?"
```

## 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

1. Configure a local profile under `~/.ms365-toolkit/profiles/<profile>/config.toml`
2. Authenticate with:

```bash
uvx --from ms365-toolkit@latest ms365-toolkit auth login
```

3. Build a local mailbox index:

```bash
uv run --with '.[dev]' ms365-toolkit sync-mail-index
```

4. Export thread candidates for labeling:

```bash
uv run --with '.[dev]' ms365-toolkit export-label-candidates --top 30 --sync-index
```

5. Review and label locally:

```bash
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.
