mcp-server-microsoft365
TypeScript MCP server for Microsoft 365 / Entra ID via Microsoft Graph. It runsapp-only (client-credentials) and is intentionally boring good: typed,documented, read-first, least-privilege, credential-sane, and idempotent whereGraph allows it. Its focus is identity provisioning — create a user, assign alicense, manage group membership — plus the supporting reads you need to do thatsafely.
Disclaimer: This is an independent, unofficial project by Borgels. Borgelsis not affiliated with, endorsed by, or supported by Microsoft. "Microsoft365", "Microsoft Graph", and "Entra ID" are referenced only to describe whatthis server talks to. You need your own Entra ID app registration, and use ofMicrosoft Graph is subject to Microsoft's own terms.
Scope
Read tools (enabled by default):
get_user,list_userslist_subscribed_skus(license lookup and seat availability)list_groups,get_group,list_group_membersget_user_license_details
Write tools (disabled by default):
create_userupdate_user(PATCH attributes on an existing user, incl.accountEnabledto enable/disable — e.g. activate a pre-created hire or disable a leaver)set_managerassign_license,remove_licenseadd_group_member,remove_group_memberset_usage_locationcreate_temporary_access_pass(passwordless first sign-in / MFA setup; regenerated until alphanumeric),delete_temporary_access_passactivate_pim_role(just-in-time PIM self-activation of an eligible directory role, e.g. Authentication Administrator for TAP — delegated mode only)
Authentication modes
The server authenticates to Graph in one of three ways, chosen by which optionsare set (never as tool arguments):
- App-only (client credentials) —
MS_TENANT_ID+MS_CLIENT_ID+MS_CLIENT_SECRET. Acts as the app; requires the application permissions below. - Delegated (on-behalf-of a user) — a
refreshTokenoption /MS_REFRESH_TOKEN(plus the app'sMS_CLIENT_ID/MS_CLIENT_SECRET/MS_TENANT_ID). The server mints short-lived delegated access tokens via therefresh_tokengrant, so calls act as the consenting user and are bounded by that user's own roles. Entra rotates the refresh token on use; the latest is exposed on the client (latestRefreshToken) so the host can persist it.activate_pim_roleonly works in this mode. - Static token — a pre-fetched
MS_ACCESS_TOKEN/accessToken(short-circuits the grants).
Least-privilege application permissions
Grant the app registration only what it needs, then admin-consent:
| Task | Application permission |
|---|---|
| Read users and groups | User.Read.All |
| Create users | User.Create (or User.ReadWrite.All) |
| Update users / set manager / enable-disable | User.ReadWrite.All |
| Read license/SKU state | LicenseAssignment.Read.All |
| Assign / remove licenses | LicenseAssignment.ReadWrite.All |
| Manage group membership | GroupMember.ReadWrite.All |
| Create / delete Temporary Access Pass | UserAuthenticationMethod.ReadWrite.All |
JIT PIM role activation (activate_pim_role, delegated) |
delegated RoleManagement.ReadWrite.Directory (bounded by the user's eligible roles) |
User.Create is narrower than User.ReadWrite.All; use it when you only need toprovision new users. update_user and set_manager require User.ReadWrite.All.Reading SKUs (/subscribedSkus) is covered by LicenseAssignment.Read.All.
Temporary Access Pass additionally requires the app to hold the AuthenticationAdministrator (or Privileged Authentication Administrator) directory role —UserAuthenticationMethod.ReadWrite.All alone is not sufficient to manage anotheruser's authentication methods. Assign the role to the app's service principal inEntra ID → Roles and administrators, and ensure the tenant's TAP policy isenabled (it also caps lifetimeInMinutes).
App registration and admin consent
- In the Entra admin center, register an application (single tenant).
- Under Certificates & secrets, create a client secret and copy its value.
- Under API permissions, add the Microsoft Graph Application permissionsabove, then click Grant admin consent.
- Copy the Directory (tenant) ID and Application (client) ID.
Setup
Install dependencies and build the CLI:
npm install
npm run build
Provide credentials through the environment. The server never accepts them astool arguments.
export MS_TENANT_ID="contoso.onmicrosoft.com" # or the tenant GUID
export MS_CLIENT_ID="your-application-client-id"
export MS_CLIENT_SECRET="your-client-secret"
Optional settings:
export MS_GRAPH_BASE_URL="https://graph.microsoft.com/v1.0"
export MS_TIMEOUT_MS=30000
# export MS_AUTHORITY_HOST="https://login.microsoftonline.com"
If another process manages tokens, provide a bearer token directly:
export MS_ACCESS_TOKEN="your-access-token"
Claude or Cursor config
Use the stdio server for local MCP clients:
{
"mcpServers": {
"microsoft365": {
"command": "node",
"args": ["/absolute/path/to/mcp-server-microsoft365/dist/transports/stdio.js"],
"env": {
"MS_TENANT_ID": "contoso.onmicrosoft.com",
"MS_CLIENT_ID": "your-application-client-id",
"MS_CLIENT_SECRET": "your-client-secret"
}
}
}
}
During development:
{
"mcpServers": {
"microsoft365": {
"command": "npm",
"args": ["run", "dev", "--prefix", "/absolute/path/to/mcp-server-microsoft365"],
"env": {
"MS_TENANT_ID": "contoso.onmicrosoft.com",
"MS_CLIENT_ID": "your-application-client-id",
"MS_CLIENT_SECRET": "your-client-secret"
}
}
}
}
Streamable HTTP transport
npm run dev:http
The HTTP transport binds to 127.0.0.1 by default and serves POST /mcp. SetMCP_HTTP_TOKEN to require bearer auth, and MCP_ALLOWED_ORIGINS forbrowser-based local clients. MCP_MAX_BODY_BYTES defaults to 10485760(10 MiB).
Provisioning workflow
list_subscribed_skusto see which licenses exist and how many seats arefree.create_userwith adisplayNameanduserPrincipalName. A strongtemporary password is generated when you do not supply one, and returned inthe result withforceChangePasswordNextSignInset. Store it securely andshare it out-of-band.create_userfails clearly if the UPN already exists.assign_licensewith a friendly name (E3), askuPartNumber(ENTERPRISEPACK), or a SKU GUID. PassusageLocationon the same call for abrand-new user.add_group_memberto place the user in the right groups. It is idempotent:an already-a-member response is treated as success.
The usageLocation gotcha
Microsoft Graph rejects a license assignment for a user with no usageLocation(the classic "License assignment failed because of an invalid usage location"error). This server sets usageLocation first:
- If you pass
usageLocationtoassign_license, it isPATCHed onto the userbefore the license is assigned. - If you do not, the server reads the user's current
usageLocationand returnsa clear, actionable error when it is missing — rather than letting Graph failcryptically.
usageLocation is an ISO 3166-1 alpha-2 country code, e.g. DK.
License resolution
assign_license / remove_license accept a friendly name, a skuPartNumber,or a SKU GUID. The server fetches /subscribedSkus (no $filter; it matchesclient-side) and:
- matches a GUID directly against
skuId; - otherwise matches
skuPartNumbercase-insensitively; - otherwise maps a friendly name (e.g.
E3→ENTERPRISEPACK/SPE_E3,E5→ENTERPRISEPREMIUM/SPE_E5) to the first candidate the tenant owns; - checks availability (
prepaidUnits.enabled - consumedUnits) and refuses toassign when no seats are free.
Rate limiting
On HTTP 429, the raised GraphHttpError carries the Retry-After value and theGraph request-id. The Borgels connector layer uses these to retry.
Borgels gateway
The ./gateway subpath exports the Borgels gateway contract:
import { createMicrosoft365Gateway, microsoft365GatewayTools } from 'mcp-server-microsoft365/gateway';
const gateway = createMicrosoft365Gateway({ tenantId, clientId, clientSecret });
const result = await gateway.callTool('list_subscribed_skus');
microsoft365GatewayTools lists the same tool surface as the MCP server, withriskLevel and enabledByDefault so the gateway can gate writes. Read tools areenabledByDefault: true; every write tool is enabledByDefault: false.
Live smoke test
The normal test suite uses mocked fetch. With real credentials in theenvironment you can run a read-only live smoke test that counts subscribed SKUs:
npm run smoke:live
Security and audit
- Credentials are read from environment variables only.
MS_GRAPH_BASE_URLmust behttps://; loopbackhttp://is allowed for localmocks.- Formatted errors redact Authorization headers, client secrets, access tokens,and token-like material.
- Write tools are disabled by default and surfaced with write/destructiveannotations.
- The Streamable HTTP transport binds to
127.0.0.1by default.
Security reports: [email protected].