exchange-online-mcp
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@exchange-online-mcpConvert alice@contoso.com to a shared mailbox and hide her from the GAL."
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
exchange-online-mcp
Exchange Online MCP server — exposes the mailbox, distribution-group and mobile-device operations that Microsoft Graph cannot express, as MCP tools over Streamable HTTP.
MSPbots Connector
The MCP Gateway built for MSPs: visible, controlled, and your data stays where it belongs.
Get started at connect.mspbots.ai →
MSPbots Connector gives your AI clients one MCP URL to reach your entire MSP stack (RMM, PSA, documentation, security, finance), and gives you control over who can call which tool and a record of everything that was called.
Hosted service: connect.mspbots.ai · Free to use.
Why Connector

Visible | Every tool call is logged: who, which vendor, when. Usage dashboard, CSV export, SIEM streaming. |
Controlled | Role-based, per-tool allowlist. Deny by default. |
Protected | Credentials encrypted per record. AI clients never see your vendor passwords or tokens. |
MSP-native | Built for the MSP stack only: RMM, PSA, documentation, security, finance. |
Quick start
Sign up at connect.mspbots.ai.
Connect your vendors. Pick from 150+ connectors and authorize each one on the vendor's own page, or bring your own API keys. No extra vendor accounts needed.
Set access. Create roles and decide which tools each role may call. Nothing is allowed until you allow it.
Add your MCP URL to your AI client. Copy the URL from your Connector dashboard into Claude, ChatGPT, Cursor, VS Code, Gemini, Codex, or any MCP-capable client.
Ask your AI to do real work, and watch every call appear in the audit log.
Features
Unified gateway
One MCP URL for all your AI clients
150+ connectors across the MSP stack
Custom MCP servers supported, free
Authorization happens on each vendor's own page; the AI never sees passwords or tokens
Bring your own credentials: use the vendor API keys you already have
Multiple accounts per app; connect once, reuse across AI clients
Tool search: finds the right action for the task
One sign-in; vendor credentials are injected behind the scenes and OAuth tokens refresh automatically
Visibility
Audit log for every tool call (who, which vendor, when)
Usage dashboard: call volume, top vendors, error rate, per-user activity
Export audit logs to CSV, or stream to Loki, Graylog, or LogScale (SIEM)
Optional recording of tool arguments and result summaries (off by default, enabled per organization)
Control
Per-role, per-tool allowlist, deny by default
owner / admin / member roles
SCIM provisioning with Entra ID, Okta, JumpCloud, and Google Workspace
Security
Credentials encrypted one record at a time (AES-256-GCM, per-record key)
AI clients authenticate with OAuth 2.1 + PKCE
Vendor responses are scanned for prompt-injection patterns and returned as structured data. This is one layer of defense, not a guarantee against every attack.
Static egress IP for customer firewall and API allowlisting
Pricing
Free.
Links: connect.mspbots.ai · mspbots.ai
Related MCP server: ms-teams-mcp
What this is for
Microsoft 365 user offboarding needs a handful of actions that only Exchange owns:
Action | Cmdlet behind it | Why Graph cannot do it |
Read a mailbox's type, size, archive and hold state |
| Graph has no mailbox management object; usage is only available as a tenant-level report with up to 48h latency, unusable as a pre-action gate |
Convert a user mailbox to a shared mailbox |
| The Entra user object has no concept of mailbox type |
Hide a mailbox from the global address list |
| Graph's |
Remove a member from a distribution / mail-enabled security group |
| Graph rejects membership writes on these Exchange-owned objects |
List / remove a mailbox's mobile devices |
| Graph exposes Intune |
Use this server alongside a Microsoft Graph MCP server: user accounts, sign-in blocking, licences, Microsoft 365 groups and security groups stay on the Graph side.
No PowerShell runtime is involved. Every cmdlet runs through the same REST entry point the Exchange Online PowerShell module itself uses:
POST https://outlook.office365.com/adminapi/beta/{tenant}/InvokeCommand
{"CmdletInput": {"CmdletName": "Get-Mailbox", "Parameters": {"Identity": "alice@contoso.com"}}}Tools
Tool | Semantics |
| read-only. Returns the mailbox, its statistics, and a |
| write, destructive. |
| write, idempotent, reversible |
| write, idempotent, reversible. Forwards to a recipient inside the tenant only ( |
| write, idempotent. Repeating an add for an existing member reports |
| write, destructive. One named member per call — no bulk removal |
| read-only. Returns the mailbox's automatic-reply state, audience, both message bodies, and the scheduled window. |
| write, idempotent. Only supplied fields change — unsupplied fields keep their current value. |
| read-only. |
| write, destructive. Removes the mailbox partnership only — it does not wipe data on the handset |
| write. Grants the trustee FullAccess (fixed, not caller-settable). Errors if already granted — check with |
| write, destructive. Revokes the trustee's FullAccess (fixed). Errors if not currently granted |
| write. Grants the trustee SendAs (fixed, not caller-settable). Errors if already granted — check with |
| write, destructive. Revokes the trustee's SendAs (fixed). Errors if not currently granted |
| read-only. Lists trustees holding FullAccess, or narrows to one trustee if |
| read-only. Lists trustees holding SendAs, or narrows to one trustee if |
Cmdlet output is projected onto a field whitelist and the @odata.type companion keys
are stripped, so a typical response is under 1.5 KB instead of the ~40 KB a raw
Get-Mailbox returns.
Credentials (HTTP headers)
The app-only credential is either a certificate or a client secret — send one, not both — and this server mints the access token from it per request. Credentials are read from headers only; there is no environment-variable fallback, and nothing is cached between requests.
The Exchange Online PowerShell module accepts only certificates for app-only sign-in,
which is why Exchange app-only access is often described as certificate-only. That is a
module limitation: the admin REST endpoint used here takes an ordinary
client_credentials token, and the Admin API authentication
reference
documents client_secret alongside the certificate assertion. The secret form has not
yet been confirmed against the undocumented adminapi/beta InvokeCommand path this
server calls, so the certificate form remains the proven one.
Header | Required | Meaning | Where it comes from |
| Yes | Tenant GUID or default domain ( | Entra admin center → Overview |
| Yes | Application (client) ID of the registered app | Entra admin center → App registrations → your app → Overview |
| One of the two | Base64 of the certificate and private key: either a PEM bundle or a PKCS#12 ( | Generated when you create the app credential (see below) |
| One of the two | Client secret value of the registered app | Entra admin center → your app → Certificates & secrets → Client secrets |
| No | Password for the | — |
| No | Overrides the | — |
Missing the tenant or application id — or both credential headers at once — on /mcp
returns 401 with the missing names listed. When a certificate and a secret both
arrive, the certificate wins.
Tenant setup
Entra admin center → App registrations → New registration (single tenant).
API permissions → Add a permission → APIs my organization uses → Office 365 Exchange Online → Application permissions →
Exchange.ManageAsApp→ Grant admin consent.Create the app credential. Either a client secret (Certificates & secrets → Client secrets → New client secret → copy the value into
X-Exo-Client-Secret), or a certificate whose public half is uploaded to the app (Certificates & secrets → Certificates → Upload certificate):openssl req -x509 -newkey rsa:2048 -sha256 -days 730 -nodes \ -keyout exo.key -out exo.crt -subj "/CN=mspbots-exo-mcp" openssl pkcs12 -export -out exo.pfx -inkey exo.key -in exo.crt # upload exo.crt base64 -w0 exo.pfx # X-Exo-CertificateA PEM bundle works just as well:
base64 -w0 <(cat exo.key exo.crt).Assign an Exchange role to the service principal — Entra admin center → Roles and administrators, or Exchange admin center → Roles → Admin roles.
Recipient Managementcovers every tool here;View-Only Organization Managementis enough for the two read-only tools. Without a role assignment every cmdlet returnsunauthorized.
Both credential forms expire: rotate the certificate before notAfter and re-upload it,
or renew the secret before its expiry. One credential per tenant — this server never
shares credentials across tenants.
Endpoints
Endpoint | Behaviour |
| MCP Streamable HTTP. Requires the credential headers |
|
|
Environment variables
Variable | Default | Purpose |
|
| Listening host |
|
| Listening port |
|
| Admin endpoint host; override for sovereign clouds (GCC High, DoD, 21Vianet) |
|
| Entra login host; same reason |
Unknown environment variables are ignored, never fatal. No credential ever comes from the environment.
Errors
Tools never raise; they return a JSON envelope as their string result:
{"error": {"code": "unauthorized", "message": "...", "retryable": false}}code is one of not_configured, unauthorized, not_found, invalid_argument,
rate_limited, upstream_error, dynamic_group_unsupported,
directory_synced_group_unsupported, microsoft_365_group_unsupported. The last three
are returned only by exo_add_distribution_group_member, for the three group types it
cannot manage. Exchange reports a missing recipient or device as a
400 carrying a cmdlet exception; those are normalised to not_found. An empty list is
a successful empty result, never not_found.
One upstream quirk shapes the transport: Exchange denies an unroled app with a 403 whose
body is Content-Length bytes of NUL, labelled Content-Encoding: gzip. Left alone that
makes httpx raise DecodingError while reading the body — a subclass of RequestError —
so the status would be lost and a deterministic 403 retried as a network fault. Admin
calls therefore send Accept-Encoding: identity, statuses with no usable body get a
message derived from the status, and an unreadable body is never reported as an empty
result.
Run it
docker compose up --build # or: docker build --platform linux/amd64 -t exchange-online-mcp:dev .
curl http://localhost:8080/healthuv sync
uv run python -m exo_mcpTests
uv run pytest # 84 unit tests, no network
docker build --platform linux/amd64 -t exchange-online-mcp:dev .
uv run python tests/mock_e2e/run_e2e.py # real container, mocked Entra + Exchangetests/mock_e2e/ runs the built image against a local stand-in for Entra ID and the
admin endpoint: it generates a throwaway certificate, verifies the client assertion the
container signs (RS256, x5t, audience), and drives
401 → initialize → tools/list → tools/call for all ten tools. No customer tenant
required.
Delivery checklist (SOP §14 self-assessment)
Integration contract
POST /mcpStreamable HTTP;GET /healthreturns 200{"status":"ok"}, local-only probeDefaults to
0.0.0.0:8080Unknown environment variables ignored (
SettingsConfigDict(extra="ignore"))No session stickiness, no local persistent state
Credentials read from headers only — no env fallback, no credential fields in config
Header names follow
X-<Vendor>-<Credential>and match this README character for characterMissing headers → 401 listing them
Request-scoped isolation via
contextvars, reset infinally; no global credential stateNo cross-request caching of tenant data or derived tokens (one token exchange per tool call)
DNS-rebinding protection disabled
MCP app's lifespan mounted on the outer Starlette app
tools/callverified over real HTTP with headers (tests/mock_e2e/)
Errors and network
Structured error envelope, fixed code vocabulary, no exceptions raised
Empty results are empty, not
not_found; messages carry no credentialsOutbound timeouts (connect 5s / read 30s; token exchange read 15s)
Limited retry with backoff on 429/5xx honouring
Retry-After; worst case well under 120sOne shared connection pool, reused within a request
Container and deliverables
docker build --platform linux/amd64builds and runsMulti-stage build, production stage runs as non-root
appcurlinstalled in the production stageEXPOSE 8080, defaultMCP_HTTP_PORT/MCP_HTTP_HOST,HEALTHCHECKconfigureduv.lockcommitted, no private dependency sourcesCredential header table present (above)
Agent-facing design
10 tools, modelled on the offboarding flow rather than on endpoints
Every description ≤ 500 chars (longest 404), first line ≤ 100 chars
Service-level
instructionsprovided (1027 chars)Tools self-describing; required/optional parameters explicit
All tools prefixed
exo_exo_list_mobile_deviceshaslimit(default 50, hard cap 200) withhas_moreReturns capped at 20,000 chars, compact separators,
ensure_ascii=FalsereadOnlyHint/destructiveHint/idempotentHintset on every toolWrite tools justified by the offboarding SOP; single named resource only, no bulk deletes
Business flow run by an agent against a real tenant — pending a customer sandbox
Logging and sensitive information
No credentials, tokens or certificate material logged or echoed in error messages
No real credentials anywhere in the repo; certificates are generated per test run
.env/.venv/*.pem/*.pfx/*.keyexcluded in.gitignoreand.dockerignore
Known follow-ups for the first real tenant
Whether
X-AnchorMailboxis required for every cmdlet under app-only auth, and whether the tenant-GUID form (which yields no anchor) is sufficient.BypassSecurityGroupManagerCheckis now opt-in (PRD-20394 INT finding, 2026-10-14): with the app holding only Exchange Recipient Administrator, any call that carried the switch — even set to false — was refused with 403. The switch is sent only when the caller passesbypass_security_group_manager_check=true; a denial then reportsbypass_not_permitted, and Exchange's owner check reportsnot_group_manager. Still to confirm on a real tenant: whether Recipient Management can change a group's membership without the switch when the app is not a group owner.
This server cannot be deployed
Maintenance
Related MCP Connectors
Manage Microsoft 365 email, calendar, contacts and inbox rules via the Graph API with OAuth 2.0.
Hosted multi-user MCP for Microsoft Exchange - on-premises, hybrid and Online, per-user rights
Manage authorized OlaMail inboxes, messages, and email drafts through secure OAuth MCP tools.
Query, browse, and automate OmegaAI workspaces from any MCP client. Streamable HTTP with OAuth 2.0.
Related MCP Servers
- AlicenseAqualityBmaintenance22 consolidated MCP tools for email, calendar, contacts, folders, rules, categories, and settings with safety controls (dry-run preview, rate limiting, recipient allowlists) and MCP annotations on every tool. Works with personal Outlook.com and work/school Microsoft 365 accounts.22232 npm39MIT
- AlicenseBqualityDmaintenanceMCP server for Microsoft Teams that exposes 73 tools to manage teams, channels, chats, messages, meetings, planner, calendar, apps, tabs, scheduling, search, and authentication via the Graph API.7310 npmMIT
- AlicenseNot gradedqualityBmaintenanceExposes Azure Entra ID user and license management as MCP tools over HTTP-SSE, enabling operations such as user creation, group assignment, and license management via Microsoft Graph API.Apache 2.0
- AlicenseBqualityAmaintenanceProduction-grade MCP server for Microsoft 365, providing tools to manage Email, Calendar, Contacts, OneDrive, Teams, Tasks, and Users via delegated OAuth.4719 npm1MIT