Skip to main content
Glama
MSPbotsAI

exchange-online-mcp

by MSPbotsAI

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.

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

Get-Mailbox + Get-MailboxStatistics

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

Set-Mailbox -Type Shared

The Entra user object has no concept of mailbox type

Hide a mailbox from the global address list

Set-Mailbox -HiddenFromAddressListsEnabled

Graph's showInAddressList is not authoritative for mailbox-enabled users

Remove a member from a distribution / mail-enabled security group

Remove-DistributionGroupMember

Graph rejects membership writes on these Exchange-owned objects

List / remove a mailbox's mobile devices

Get-MobileDevice / Remove-MobileDevice

Graph exposes Intune managedDevices only, never Exchange ActiveSync device partnerships

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"}}}

Related MCP server: ms-teams-mcp

Tools

Tool

Semantics

exo_get_mailbox(identity, include_statistics=True)

read-only. Returns the mailbox, its statistics, and a shared_conversion block (size_gb, over_limit, archive_enabled, litigation_hold_enabled, license_required) answering "can this become a licence-free shared mailbox?"

exo_convert_mailbox_to_shared(identity)

write, destructive. Set-Mailbox -Type Shared, then reads the mailbox back to confirm

exo_set_mailbox_hidden(identity, hidden)

write, idempotent, reversible

exo_remove_distribution_group_member(group, member, bypass_security_group_manager_check=True)

write, destructive. One named member per call — no bulk removal

exo_list_mobile_devices(mailbox, limit=50)

read-only. limit 1–200, results truncated with has_more

exo_remove_mobile_device(device_id)

write, destructive. Removes the mailbox partnership only — it does not wipe data on the handset

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)

Exchange Online app-only access accepts certificates only — not client secrets — so the certificate itself is the credential and this server mints the access token per request. Credentials are read from headers only; there is no environment-variable fallback, and nothing is cached between requests.

Header

Required

Meaning

Where it comes from

X-Exo-Tenant-Id

Yes

Tenant GUID or default domain (contoso.onmicrosoft.com). A domain also lets the server anchor app-only calls on the tenant system mailbox, which some cmdlets need

Entra admin center → Overview

X-Exo-Client-Id

Yes

Application (client) ID of the registered app

Entra admin center → App registrations → your app → Overview

X-Exo-Certificate

Yes

Base64 of the certificate and private key: either a PEM bundle or a PKCS#12 (.pfx) blob

Generated when you create the app credential (see below)

X-Exo-Certificate-Password

No

Password for the .pfx / encrypted PEM. Omit when there is none

X-Exo-Anchor-Mailbox

No

Overrides the X-AnchorMailbox routing hint, e.g. UPN:admin@contoso.com

Missing any required header on /mcp returns 401 with the missing names listed.

Tenant setup

  1. Entra admin center → App registrations → New registration (single tenant).

  2. API permissions → Add a permission → APIs my organization uses → Office 365 Exchange Online → Application permissions → Exchange.ManageAsAppGrant admin consent.

  3. Create the certificate and upload the public half 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-Certificate

    A PEM bundle works just as well: base64 -w0 <(cat exo.key exo.crt).

  4. Assign an Exchange role to the service principal — Entra admin center → Roles and administrators, or Exchange admin center → Roles → Admin roles. Recipient Management covers every tool here; View-Only Organization Management is enough for the two read-only tools. Without a role assignment every cmdlet returns unauthorized.

Certificates expire: rotate them before notAfter and re-upload. One certificate per tenant — this server never shares credentials across tenants.

Endpoints

Endpoint

Behaviour

POST /mcp

MCP Streamable HTTP. Requires the credential headers

GET /health

{"status": "ok"}. Purely local probe — never touches Exchange Online

Environment variables

Variable

Default

Purpose

MCP_HTTP_HOST

0.0.0.0

Listening host

MCP_HTTP_PORT

8080

Listening port

EXO_BASE_URL

https://outlook.office365.com

Admin endpoint host; override for sovereign clouds (GCC High, DoD, 21Vianet)

ENTRA_LOGIN_BASE_URL

https://login.microsoftonline.com

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

Run it

docker compose up --build          # or: docker build --platform linux/amd64 -t exchange-online-mcp:dev .
curl http://localhost:8080/health
uv sync
uv run python -m exo_mcp

Tests

uv run pytest                                   # 50 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 + Exchange

tests/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 six tools. No customer tenant required.

Delivery checklist (SOP §14 self-assessment)

Integration contract

  • POST /mcp Streamable HTTP; GET /health returns 200 {"status":"ok"}, local-only probe

  • Defaults to 0.0.0.0:8080

  • Unknown 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 character

  • Missing headers → 401 listing them

  • Request-scoped isolation via contextvars, reset in finally; no global credential state

  • No 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/call verified 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 credentials

  • Outbound timeouts (connect 5s / read 30s; token exchange read 15s)

  • Limited retry with backoff on 429/5xx honouring Retry-After; worst case well under 120s

  • One shared connection pool, reused within a request

Container and deliverables

  • docker build --platform linux/amd64 builds and runs

  • Multi-stage build, production stage runs as non-root app

  • curl installed in the production stage

  • EXPOSE 8080, default MCP_HTTP_PORT/MCP_HTTP_HOST, HEALTHCHECK configured

  • uv.lock committed, no private dependency sources

  • Credential header table present (above)

Agent-facing design

  • 6 tools, modelled on the offboarding flow rather than on endpoints

  • Every description ≤ 500 chars (longest 404), first line ≤ 100 chars

  • Service-level instructions provided (1027 chars)

  • Tools self-describing; required/optional parameters explicit

  • All tools prefixed exo_

  • exo_list_mobile_devices has limit (default 50, hard cap 200) with has_more

  • Returns capped at 20,000 chars, compact separators, ensure_ascii=False

  • readOnlyHint / destructiveHint / idempotentHint set on every tool

  • Write 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 / *.key excluded in .gitignore and .dockerignore

Known follow-ups for the first real tenant

  • Whether X-AnchorMailbox is required for every cmdlet under app-only auth, and whether the tenant-GUID form (which yields no anchor) is sufficient.

  • Whether BypassSecurityGroupManagerCheck is needed for mail-enabled security groups in practice, or should become opt-in.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Connectors

Related MCP Servers

  • A
    license
    A
    quality
    B
    maintenance
    MCP server for Microsoft Outlook via Graph API. 20 consolidated 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.
    22
    1,037
    36
    MIT
  • A
    license
    Not graded
    quality
    B
    maintenance
    Exposes 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

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/MSPbotsAI/exchange_online_mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server