Skip to main content
Glama
usedrobot

Microsoft 365 remote MCP server

by usedrobot

Microsoft 365 remote MCP server

A remote MCP server that gives Claude access to Microsoft 365 — mail, calendar, files, and Teams chat — with per-user identity. Each person signs in through Microsoft as themselves, and the server exchanges that sign-in for a Graph token via the On-Behalf-Of flow, so every call runs with exactly that person's permissions. It does not hold one app identity with standing access to the whole tenant: if a user cannot see a message, neither can the server acting for them.

Built for and running in production at a small nonprofit.

Design stance

  • Acts as the user, not as the app. Sign-in is a PKCE proxy to Microsoft Entra, and every Graph call goes through On-Behalf-Of exchange of the caller's own token. The server never holds standing access to anyone's mail — no application permissions, no service-account mailbox, nothing to steal that would read a mailbox on its own.

  • Drafts only. Mail.Send is not requested, so it cannot be granted. The model composes; a human opens the draft and presses send. A compromised prompt or a confused model can at worst leave an unwanted draft in your Drafts folder. This is a deliberate boundary, not a missing feature.

  • Tenant-pinned. common and organizations are rejected at startup, and the tenant must parse as a GUID. A wildcard tenant would let Entra accept tokens issued by any organization — the single most damaging misconfiguration available on a public endpoint, so it is made unreachable rather than documented as a caution.

Architecture

User → Claude → /authorize → Microsoft Entra (login + consent)
Claude → /token  (two-leg PKCE proxy → Entra)          ← server holds the confidential client
Claude → /mcp  (Bearer = user token, aud = this app)
   └─ verify.ts: validate JWT (sig/iss/aud/exp + tenant pin + scope)
   └─ obo.ts: exchange for a Graph token (acts as the user)
   └─ tools.ts: call Microsoft Graph

Entra supports neither Dynamic Client Registration nor Client ID Metadata Documents, which is why this server is itself the OAuth authorization server Claude talks to: it exposes /register and brokers every flow down to one pre-registered Entra confidential client.

Setup

This is not a two-minute install, and pretending otherwise would waste your time. Before it will start you need:

  • an Entra app registration in your own tenant, exposing an access_as_user scope, with a client secret;

  • tenant-admin consent on eleven delegated Graph permissions (Mail.Send is not among them, by design);

  • a public HTTPS origin Claude can reach — Claude connects to your server over the internet, so localhost is not enough outside of npm run inspector.

docs/entra-setup.md walks the registration through from scratch, including which permissions to grant and why.

Config

Copy .env.example to .env and fill it in.

Var

Required

Notes

M365_CLIENT_ID

Client id of your delegated Entra app

M365_CLIENT_SECRET

Confidential-client secret (required by OBO)

M365_TENANT_ID

Tenant GUID. common/organizations are rejected at startup.

M365_PUBLIC_URL

Public HTTPS origin Claude reaches, no trailing slash

M365_ORG_NAME

optional

Your organization's name, used in the server instructions

M365_EXTRA_INSTRUCTIONS

optional

House-style notes appended to the server instructions (tone, signatures, place names)

M365_EVENTS_LIBRARY_PATH

optional

SharePoint library the default file-search scope narrows to; unset searches the whole tenant

M365_READ_ONLY

optional

true disables every write tool

M365_DEFAULT_TZ

optional

IANA zone, default America/New_York

M365_REDACT_PII

optional

true scrubs tokens, secrets, and email addresses from logs

M365_ALLOWED_REDIRECT_URIS

optional

Comma-separated exact /authorize redirect_uri allowlist. Defaults to https://claude.ai/api/mcp/auth_callback; set this only if a different client connects.

M365_TRUST_PROXY

optional

Default off. Set to 1 (or an Express hop count) only when running behind a reverse proxy you control, so X-Forwarded-For is trusted. With the port published directly, leaving this off is what keeps the rate limits enforceable.

M365_CORS_ORIGIN

optional

CORS origin allowed to call this server, default https://claude.ai

M365_GRAPH_TIMEOUT_MS

optional

Microsoft Graph request timeout in milliseconds, default 30000

PORT

optional

default 3000

Run

npm install
npm run build
M365_CLIENT_ID=… M365_CLIENT_SECRET=… M365_TENANT_ID=<guid> M365_PUBLIC_URL=https://… npm start

Local tool inspection over stdio, without a public host:

npm run inspector

The included Dockerfile is a plain multi-stage Node build that ships to any container host. No platform deploy configuration is included on purpose — where you run it is your decision, and a checked-in deploy manifest would only encode someone else's.

Tools

Read: search_email, get_email, search_calendar, get_event, find_meeting_times, search_files, get_file, search_teams_chat — with natural-language date parsing and offset pagination. Delegated/shared access via mailboxOwner / calendarOwner is supported by the four mailbox and calendar tools — search_email, get_email, search_calendar, get_event. find_meeting_times, search_files, get_file, and search_teams_chat always act as the signed-in user.

Write (all gated behind M365_READ_ONLY): draft_email, draft_reply, create_event, update_event, cancel_event. Drafts are auto-formatted — agent hard-wraps are healed so prose does not render as a frozen narrow column on mobile — and writes honour the same delegated mailboxOwner / calendarOwner access as the reads.

There is no send tool. See Drafts only, above.

Provenance and security

The auth and transport plumbing is adapted from softeria/ms-365-mcp-server (MIT). Every copied file was reviewed line-by-line before inclusion; the attribution is in NOTICE.md and the file-by-file review in docs/copied-code-audit.md.

Hardening applied over upstream:

  • Tenant-pinned config — the tenant is required and validated as a GUID; upstream defaults to common.

  • Full local JWT validation — signature, issuer, audience, expiry, tenant, and scope are checked against Entra's JWKS with jose before any Graph call. Upstream did an expiry-only pre-check and let the downstream Graph call be the real gate.

  • OBO only — no trust-proxy or token pass-through mode.

  • Curated tool surface — a deliberately chosen set of tools rather than a generated wrapper over the whole Graph API, with no send capability anywhere in it.

Licensed MIT — see LICENSE.