office-connect
Microsoft 365 superpowers for Python and AI agents.
office-connect is a Python library and stdio MCP server that gives both human developers and AI agents structured access to Microsoft 365 — mail, calendar, Teams, chats, files, directory, profile — through the Microsoft Graph API, behind a simple token-based authentication flow.
Features
- MCP Server -- Stdio-based MCP server for seamless integration with AI assistants
- Mail -- List messages, read bodies and attachments, send, draft, reply, manage categories
- Calendar -- List calendars, query events, create events, check free/busy schedules
- Teams -- List joined teams, channels, channel messages, and team members
- Chat -- List recent 1:1, group, and meeting chats with message history
- Files / OneDrive -- Browse drives, list folders, download file content, search by name
- SharePoint -- Search sites and list document libraries
- Directory -- List organization users, resolve managers, fetch profile photos
- Profile -- Retrieve the authenticated user's profile details
- Mock Transport -- Full synthetic data layer for testing without a real O365 account
Quick Start
Installation
poetry add office-connect
MCP Server
office-connect --keyfile path/to/token.json
Token File Format
{
"app": "MyApp",
"email": "user@example.com",
"access_token": "eyJ...",
"refresh_token": "1.AUs...",
"client_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"client_secret": "...",
"tenant_id": "common"
}
Tokens are automatically refreshed (proactively when within 15 min of expiry, reactively on a 401 from Graph) and persisted back to the file (0600 permissions).
Signing in for the first time (office-connect login)
When you don't have any tokens yet, use the device-code login. The first run takes your Azure AD app credentials once and saves them; every subsequent re-auth needs no arguments at all.
# Cold start — supply the Azure AD app once
office-connect login --client-id <APP_ID> --tenant-id <TENANT_ID> [--client-secret <SECRET>]
# From there on — re-auth with zero arguments
office-connect login
# Narrow scopes (default is all eight groups)
office-connect login --scope mail --scope calendar
office-connect login prints a microsoft.com/devicelogin URL plus a short code, blocks polling Microsoft until you finish signing in, then writes two files:
~/.config/office-connect/token.json— the keyfile (access + refresh tokens), 0600 permissions~/.config/office-connect/config.json— the app config: client_id / tenant_id / optional client_secret, 0600 permissions
Both paths are configurable (--keyfile and --app-config, or env var OFFICE_CONNECT_APP_CONFIG). When login resolves credentials it tries, in order: explicit CLI args → existing keyfile → O365_CLIENT_ID etc. → app-config file. You can also pre-create the app-config file by hand — it's just a JSON object with client_id, tenant_id, and optional client_secret.
Available scope groups: profile, directory, mail, calendar, chat, teams, drive, tasks.
Your Azure AD app registration must have "Allow public client flows" enabled in its authentication manifest for device-code flow to work.
Refreshing tokens during a running session
You don't have to. The MCP keeps tokens fresh on its own — within a session via in-process refresh, across restarts via the keyfile. Manual top-ups only matter if even the refresh token has been invalidated (typically after ~90 days of inactivity, or by tenant policy).
If a host application exports tokens through an admin endpoint, drop the exported JSON at the canonical keyfile location with:
office-connect import-token ~/Downloads/token_export.json
# override destination with --dest /custom/path/token.json
Both login and import-token write atomically with 0600 permissions. The MCP server compares the keyfile's mtime before each tool call; once the file changes, the next tool invocation rebuilds the Graph instance from the new contents — Claude Desktop, Cursor, etc. do not need to be restarted.
Permission tiers — what Claude is allowed to do
Three tiers, from most to least restrictive:
| tier | what it allows |
|---|---|
read_only |
list/get/search/peek — no mutation of Microsoft 365 state |
drafts (default) |
everything read_only does, plus creating and updating draft emails. No sending. |
all |
everything drafts does, plus sending mail, moving/deleting mail, flagging read, creating calendar events |
Three places can set the tier — the most restrictive of the ones that are set wins:
-
MCP launcher CLI flag:
--permission-level read_only|drafts|allin the args list passed by Claude Desktop / Cursor / etc. -
Environment variable:
OFFICE_CONNECT_PERMISSION_LEVEL=read_only -
Global policy file: a JSON object at
~/.config/office-connect/policy.json(overridable via--policy-file PATHor$OFFICE_CONNECT_POLICY):{ "permission_level": "drafts" }
The policy file acts as a host-wide ceiling: regardless of how any individual MCP launcher is configured, the resolver clamps the effective tier down to the most restrictive value found. A launcher can always tighten further on top.
Tools above the effective tier are removed from list_tools and refused by call_tool (defense in depth, fail-closed for unknown tool names).
Mock Transport
A full mock layer for development and testing — no real O365 account needed.
from office_con.msgraph.ms_graph_handler import MsGraphInstance
from office_con.testing.fixtures import default_mock_profile
profile = default_mock_profile()
graph = MsGraphInstance(endpoint="https://graph.microsoft.com/v1.0/")
graph.enable_mock(profile)
# Now use graph exactly like the real thing
mail = graph.get_mail()
inbox = await mail.email_index_async(limit=10)
The mock provides:
- 18+ inbox messages (rich HTML with signatures, newsletters, notifications)
- 9 mail folders with subfolders, fake downloadable attachments
- 127 calendar events across 3 months (OOF, Teams calls, tentative, free blocks)
- 25 directory users with org hierarchy, departments, and profile photos
- Teams, chats, categories, OneDrive stubs
- Synthetic JWT tokens
Face photos can be loaded from JPEG files via set_faces_dir() or the FACES_DIR env var. Falls back to generated SVG initials.
Safety: mock is automatically blocked on Azure App Service and production URLs.
Project Structure
office-connect/
├── office_con/
│ ├── auth/ # Azure AD OAuth, scopes, background refresh
│ ├── db/ # Company directory builder and storage
│ ├── msgraph/ # MS Graph API handlers
│ │ ├── ms_graph_handler.py # Central class: MsGraphInstance
│ │ ├── mail_handler.py # Send, draft, reply, list, categories
│ │ ├── calendar_handler.py # Events, schedules, timezones
│ │ ├── directory_handler.py # Users, managers, photos
│ │ ├── teams_handler.py # Teams, channels, messages
│ │ ├── chat_handler.py # 1:1, group, meeting chats
│ │ ├── files_handler.py # OneDrive, SharePoint
│ │ └── profile_handler.py # /me profile
│ ├── testing/ # Mock transport, fixtures, synthetic tokens
│ ├── utils/ # Excel parser, health check
│ └── mcp_server.py # MCP server entry point + CLI
├── tests/
├── docs/
├── pyproject.toml
└── LICENSE
Development
poetry install
poetry run pytest # tests (skips integration tests without token file)
poetry run ruff check # lint
MCP Tools
| Tool | Description |
|---|---|
o365_get_profile |
Current user's profile |
o365_list_mail |
List recent inbox emails |
o365_get_mail |
Single email with full body and attachments |
o365_get_mail_categories |
Outlook mail categories |
o365_list_calendars |
User's calendars |
o365_get_events |
Calendar events in a date range |
o365_get_schedule |
Free/busy availability |
o365_list_teams |
Joined Microsoft Teams |
o365_list_channels |
Channels in a team |
o365_get_channel_messages |
Channel messages |
o365_get_team_members |
Team members |
o365_list_chats |
Recent chats |
o365_get_chat_messages |
Chat messages |
o365_get_chat_members |
Chat members |
o365_get_my_drive |
Default OneDrive info |
o365_list_drive_items |
Files and folders |
o365_get_file_content |
Download file content |
o365_search_files |
Search OneDrive |
o365_search_sites |
Search SharePoint sites |
o365_get_site_drives |
Site document libraries |
o365_list_users |
Organization directory |
o365_get_user_manager |
User's manager |
License
MIT -- Copyright (c) 2026 Michael Ikemann
Metadata
Release files for office-connect 0.1.8
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| office_connect-0.1.8.tar.gz | 140.6 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| office_connect-0.1.8-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 297.3 kB
Release files / office_connect-0.1.8.tar.gz
| Download URL | office_connect-0.1.8.tar.gz |
|---|---|
| Size | 140.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
08438243a95ace02a1056212bdce2ce7c1eb436e018af32dbb5151bc5bb8aadf
|
|
BLAKE2b-256 checksum How to use checksums |
e69527471be726aca2da64a88e8fd13cad772ae748cf9c305e24b9aaaf2ca669
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
poetry/2.1.1 CPython/3.13.11 Darwin/25.2.0
|
Release files / office_connect-0.1.8-py3-none-any.whl
| Download URL | office_connect-0.1.8-py3-none-any.whl |
|---|---|
| Size | 156.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
ea76147545c12ea1351e2867c5db1a19e528e5ff432b8260ab94a45b6c40632f
|
|
BLAKE2b-256 checksum How to use checksums |
44ebd74fad455c11ad67f80d276591831ac541b02a02be2892a624eab3642556
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
poetry/2.1.1 CPython/3.13.11 Darwin/25.2.0
|
Security for office-connect 0.1.8
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.