Skip to main content

Microsoft 365 MCP server

Reach Outlook mail and calendar, OneDrive and SharePoint files, and Teams through Microsoft Graph, with every tool call running under the signed-in user's own Entra ID account.

Microsoft Graph is the API behind Microsoft 365, and the open source ms-365-mcp-server exposes it over MCP. You run that server next to the proxy over Streamable HTTP, where it signs nobody in and calls Graph with whatever bearer token arrives on the request, and LiteLLM supplies that token: it runs the Entra ID sign-in for each user, stores the resulting Graph token against that LiteLLM user, and attaches it to every tool call. No shared mailbox credential or tenant-wide application secret is involved. LiteLLM adds centralized auth, access control by key and team, cost tracking per tool call, and one audit trail across every MCP server you expose.

The server only translates MCP calls into Graph calls. Sign-in, consent, token storage, and refresh happen between LiteLLM and Entra ID, so swapping the server changes nothing about how users authenticate: any MCP server that calls Graph with the Authorization: Bearer token it receives works with the same LiteLLM configuration and a different url.

When should you use this server​

  • Give an agent the user's own inbox and calendar: what came in, what is booked, and who is free, without a shared service mailbox
  • Pull SharePoint and OneDrive documents as a retrieval step before answering, limited to what that person can already open
  • Read and post in Teams chats and channels, manage Planner and To Do tasks, and query Excel tables and OneNote pages from the same server

Key features​

  • Per-user auth through Entra ID: each caller consents once in their browser, and every tool call carries their delegated Graph permissions, so the agent sees exactly what the user sees
  • Tool surface follows the delegated permissions on the app registration, so a read-only rollout and a full read/write rollout use the same server with different scopes
  • 300+ tools across mail, calendar, contacts, OneDrive, SharePoint, Teams, Planner, To Do, OneNote, Excel, users, and groups, plus search-query and graph-batch for anything else Graph exposes
  • Self-hosted over Streamable HTTP, so Graph traffic stays inside your network

Authentication​

  • Method: OAuth 2.0 authorization code with PKCE against Microsoft Entra ID. Entra does not support dynamic client registration, so you register an app and give LiteLLM its client ID and secret. LiteLLM then publishes OAuth discovery documents for the server that point MCP clients at its own sign-in endpoints in front of Entra (per_server_oauth_discovery: true below).
  • App registration: Create one in the Microsoft Entra admin center under App registrations. You will add a Web redirect URI, a client secret, and delegated Microsoft Graph permissions.
  • Consent: Each user approves the delegated permissions on first sign-in. A tenant that restricts user consent needs an admin to grant consent once on the app registration's API permissions page.

Single sign-on through Okta or another identity provider​

Microsoft Graph accepts only access tokens that Entra ID issues, so neither an Okta token nor the credential from your LiteLLM single sign-on can stand in for the Graph token, and LiteLLM does not convert one into the other. What carries over is the sign-in itself. When your Microsoft 365 tenant federates authentication to Okta or another identity provider, the Entra sign-in LiteLLM starts redirects there like any other Microsoft 365 sign-in, so users see their usual SSO page, and Entra issues the Graph token once they return. Each user does this once; LiteLLM refreshes the token afterwards.

Three other LiteLLM auth modes come up in this setup and none of them replaces the flow above. On-behalf-of token exchange trades an Entra ID token the client already presents to LiteLLM, so it applies only when users authenticate to LiteLLM with Entra ID tokens rather than through Okta. ID-JAG gets tokens from an Okta authorization server, which Graph does not trust. OAuth passthrough forwards the client's bearer token unchanged and exchanges nothing, so the client would still have to obtain a Graph token on its own.

Endpoint​

Self-hosted MCP server (organization mode, Streamable HTTP on port 3000):

npx -y @softeria/ms-365-mcp-server --http 3000 --org-mode

The server then listens at http://localhost:3000/mcp. --http is what makes it take the bearer token on each request, which is the one LiteLLM forwards, instead of signing in on its own; it checks only that the token is present and unexpired and leaves the rest to Graph. --org-mode adds the work and school tools (Teams, SharePoint, shared mailboxes) and their permissions. Add --read-only to drop every write tool, or --enabled-tools <regex> to expose a subset, and pin a version (@softeria/ms-365-mcp-server@<version>) in production, since the tool list changes between releases. Keep the port reachable from the proxy only.


Connect via LiteLLM MCP Gateway​

info

Microsoft 365 is one of the servers that needs explicit client credentials. LiteLLM normally handles OAuth client setup through dynamic registration, as on the Atlassian and Linear servers, but Entra ID requires your own app registration, the same way Slack does.

Step 1: Register an Entra ID app​

  1. In the Microsoft Entra admin center, open App registrations and click New registration.

  2. Name it (for example LiteLLM MCP gateway - Microsoft 365) and keep Accounts in this organizational directory only unless users from other tenants should sign in.

  3. Under Redirect URI, pick the Web platform and enter {PROXY_BASE_URL}/callback:

    https://llm.example.com/callback
  4. Replace https://llm.example.com with the origin users see in their address bar. This is the value LiteLLM sends to Entra as the redirect_uri, so a mismatch fails the flow at the Microsoft sign-in page with AADSTS50011. See Reverse proxy and ingress configuration if LiteLLM sits behind an ingress.

  5. Under Certificates & secrets, create a client secret and copy its Value right away; it is shown once.

  6. Under API permissions, click Add a permission, pick Microsoft Graph, then Delegated permissions, and add the scopes for the capabilities you want, matching the table in Tools provided. A read-only rollout is openid, offline_access, User.Read, Mail.Read, Calendars.Read, Files.Read.All, and Sites.Read.All. offline_access is what lets LiteLLM refresh the token, so keep it.

  7. Click Grant admin consent if your tenant blocks users from consenting themselves.

  8. From Overview, copy the Application (client) ID and Directory (tenant) ID.

Step 2: Run the Graph MCP server​

Start the server on the proxy host or on a machine only the proxy can reach. --http makes it use the token LiteLLM forwards instead of prompting for its own sign-in, and --org-mode turns on the work and school tools:

npx -y @softeria/ms-365-mcp-server --http 3000 --org-mode

Step 3: Register the server in LiteLLM​

config.yaml
mcp_servers:
microsoft_365:
url: "http://localhost:3000/mcp"
transport: "http"
description: "Outlook mail and calendar, OneDrive and SharePoint files, and Teams through Microsoft Graph"
auth_type: oauth2
oauth2_flow: authorization_code
per_server_oauth_discovery: true
client_id: os.environ/M365_ENTRA_CLIENT_ID
client_secret: os.environ/M365_ENTRA_CLIENT_SECRET
authorization_url: "https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/authorize"
token_url: "https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/token"
scopes:
- openid
- offline_access
- User.Read
- Mail.Read
- Calendars.Read
- Files.Read.All
- Sites.Read.All

oauth2_flow: authorization_code selects the interactive per-user flow and is required; the proxy refuses to start on an auth_type: oauth2 server that omits it. per_server_oauth_discovery: true makes LiteLLM publish the OAuth discovery documents for /microsoft_365/mcp itself, with its own /microsoft_365/authorize and /microsoft_365/token endpoints fronting the Entra URLs above. Without it, the server's protected-resource metadata sends MCP clients to the gateway's shared /mcp authorization server, which signs users in to LiteLLM rather than to Entra, so the browser ends on a "The connection cannot continue" page and no Graph token is ever stored. Storing MCP servers also needs store_model_in_db: true, covered in Prerequisites.

Step 4: Set the proxy's public origin​

LiteLLM builds the redirect_uri it sends to Entra from its public origin. Set PROXY_BASE_URL to the origin you registered in Step 1 so the two match:

export PROXY_BASE_URL=https://llm.example.com

Step 5: Give users access to the server​

LiteLLM stores each Entra token against the LiteLLM user behind the key, so the key or its team needs the server in object_permission.mcp_servers, and the key needs a user behind it. A key without that completes the sign-in but has nowhere to keep the token, so the next session signs in again, and the proxy log says OAuth credential storage not authorized. See MCP Permission Management. Users who sign in to LiteLLM through SSO already have a LiteLLM user, so granting the server to the team their identity-provider group maps to is enough.

Step 6: Connect from an agent​

The gateway serves each server at http://localhost:4000/{server_name}/mcp, so microsoft_365 is reachable at http://localhost:4000/microsoft_365/mcp.

claude mcp add --transport http microsoft_365 http://localhost:4000/microsoft_365/mcp \
--header "x-litellm-api-key: Bearer $LITELLM_API_KEY"

/mcp inside Claude Code shows the server and starts the Entra sign-in. Claude Code caches the OAuth client it registered with the gateway, so if you turn on per_server_oauth_discovery after adding the server, remove it and add it again.

Users who sign in to LiteLLM through SSO have no sk- key to paste. After lite login (CLI authentication), let Claude Code fetch the header itself with headersHelper, a command whose output is the headers as JSON:

.mcp.json
{
"mcpServers": {
"microsoft_365": {
"type": "http",
"url": "http://localhost:4000/microsoft_365/mcp",
"headersHelper": "printf '{\"x-litellm-api-key\": \"Bearer %s\"}' \"$(lite auth print-token)\""
}
}
}

The Microsoft sign-in that follows is stored against the LiteLLM user that SSO created, so the same person keeps their Graph token across keys and sessions.

The first call opens a browser at the Microsoft sign-in, followed by the consent screen listing the delegated permissions from Step 1. LiteLLM stores that user's token and refreshes it afterwards, so later sessions connect without prompting. GET /v1/mcp/server/{server_id}/oauth-user-credential/status shows whether the calling user has a stored token and when it expires, and DELETE /v1/mcp/server/{server_id}/oauth-user-credential revokes it.


Tools provided​

info

The server publishes its tool definitions at runtime through tools/list, so names and fields change between releases (version 0.155.0 lists 337 tools in organization mode). The MCP Tools tab in the LiteLLM UI is the source of truth for what your tenant exposes; it lists the live tools and lets you call one with test arguments. npx -y @softeria/ms-365-mcp-server --list-permissions --org-mode prints the Graph permission set behind the full tool list, and adding --read-only prints the read-only set.

AreaTools (a sample)Delegated permissions
Maillist-mail-messages, get-mail-message, list-mail-folders, list-mail-folder-messages, send-mail, forward-mail-messageMail.Read to read, Mail.ReadWrite and Mail.Send to write
Calendarlist-calendars, get-calendar-view, list-calendar-events, find-meeting-times, create-calendar-eventCalendars.Read to read, Calendars.ReadWrite to write
OneDrivelist-drives, get-drive-root-item, list-folder-files, get-drive-item, upload-file-content, create-drive-item-share-linkFiles.Read.All to read, Files.ReadWrite to write
SharePointsearch-sharepoint-sites, get-sharepoint-site, list-sharepoint-site-drives, list-sharepoint-site-items, list-sharepoint-site-lists, get-sharepoint-site-listSites.Read.All to read, Sites.ReadWrite.All to write
Teamslist-joined-teams, list-team-channels, list-channel-messages, list-chats, list-chat-messages, send-channel-message, send-chat-messageTeam.ReadBasic.All, Channel.ReadBasic.All, ChannelMessage.Read.All, Chat.Read to read; ChannelMessage.Send, Chat.ReadWrite to write
Planner and To Dolist-planner-tasks, list-todo-task-lists, list-todo-tasks, create-planner-task, create-todo-taskTasks.Read to read, Tasks.ReadWrite to write
OneNote and Excellist-onenote-notebooks, list-onenote-pages, get-onenote-page-content, list-excel-worksheets, list-excel-tables, get-excel-rangeNotes.Read and Files.Read.All to read, Notes.ReadWrite and Files.ReadWrite to write
People and directoryget-current-user, list-users, get-user-manager, list-outlook-contacts, list-groups, list-group-membersUser.Read, User.Read.All, Contacts.Read, Group.Read.All, GroupMember.Read.All
Cross-servicesearch-query (Graph search across mail, files, sites, and people), graph-batch (several Graph calls in one round trip)Whatever the searched or batched resources need

Every tool reaches an agent as microsoft_365-<tool> (see Tool naming). Three hundred tools is more than most agents want in one context, so pair the server with Tool Search or the Semantic Filter, or run the server with --enabled-tools.

Known limitations​

The server calls Graph with any unexpired bearer token it receives and serves sign-in endpoints of its own, so it must only be reachable through the proxy. OneDrive tools return itemNotFound for a user whose OneDrive was never provisioned (they have not opened OneDrive or Office on the web yet); SharePoint document libraries are unaffected. search-query and search-sharepoint-sites take Graph search (KQL) syntax, so a bare * is rejected. Adding a permission to the app registration after users consented needs a fresh consent: revoke the stored credential with DELETE /v1/mcp/server/{server_id}/oauth-user-credential and sign in again. Entra access tokens last about an hour; LiteLLM refreshes them with the offline_access refresh token, so leaving that scope out means a sign-in prompt every hour.


Restrict who can use it

Grant the server per key or per team with object_permission, and cap call volume per server with mcp_rpm_limit, both covered in MCP Permission Management. Graph applies its own per-user throttling, so a runaway agent slows that user's own Outlook and Teams clients before anyone else's.

Put the LiteLLM key in x-litellm-api-key

Interactive OAuth needs the Authorization header free for the upstream token. If a client sends the LiteLLM API key as Authorization: Bearer sk-..., the OAuth flow never runs and LiteLLM forwards your LiteLLM key to the Graph server, which passes it to Microsoft, and Graph rejects it with InvalidAuthenticationToken. See Debugging OAuth and the MCP Troubleshooting Guide.