Skip to main content
Glama
borgels

mcp-server-microsoft365

by borgels

mcp-server-microsoft365

TypeScript MCP server for Microsoft 365 / Entra ID via Microsoft Graph. It runs app-only (client-credentials) and is intentionally boring good: typed, documented, read-first, least-privilege, credential-sane, and idempotent where Graph allows it. Its focus is identity provisioning — create a user, assign a license, manage group membership — plus the supporting reads you need to do that safely.

Disclaimer: This is an independent, unofficial project by Borgels. Borgels is not affiliated with, endorsed by, or supported by Microsoft. "Microsoft 365", "Microsoft Graph", and "Entra ID" are referenced only to describe what this server talks to. You need your own Entra ID app registration, and use of Microsoft Graph is subject to Microsoft's own terms.

Scope

Read tools (enabled by default):

  • get_user, list_users

  • list_subscribed_skus (license lookup and seat availability)

  • list_groups, get_group, list_group_members

  • get_user_license_details

Write tools (disabled by default):

  • create_user

  • update_user (PATCH attributes on an existing user, incl. accountEnabled to enable/disable — e.g. activate a pre-created hire or disable a leaver)

  • set_manager

  • assign_license, remove_license

  • add_group_member, remove_group_member

  • set_usage_location

  • create_temporary_access_pass (passwordless first sign-in / MFA setup; regenerated until alphanumeric), delete_temporary_access_pass

  • activate_pim_role (just-in-time PIM self-activation of an eligible directory role, e.g. Authentication Administrator for TAP — delegated mode only)

Related MCP server: Microsoft 365 Core MCP Server

Authentication modes

The server authenticates to Graph in one of three ways, chosen by which options are 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 refreshToken option / MS_REFRESH_TOKEN (plus the app's MS_CLIENT_ID/MS_CLIENT_SECRET/MS_TENANT_ID). The server mints short-lived delegated access tokens via the refresh_token grant, 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_role only 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 to provision 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 Authentication Administrator (or Privileged Authentication Administrator) directory role — UserAuthenticationMethod.ReadWrite.All alone is not sufficient to manage another user's authentication methods. Assign the role to the app's service principal in Entra ID → Roles and administrators, and ensure the tenant's TAP policy is enabled (it also caps lifetimeInMinutes).

  1. In the Entra admin center, register an application (single tenant).

  2. Under Certificates & secrets, create a client secret and copy its value.

  3. Under API permissions, add the Microsoft Graph Application permissions above, then click Grant admin consent.

  4. 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 as tool 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. Set MCP_HTTP_TOKEN to require bearer auth, and MCP_ALLOWED_ORIGINS for browser-based local clients. MCP_MAX_BODY_BYTES defaults to 10485760 (10 MiB).

Provisioning workflow

  1. list_subscribed_skus to see which licenses exist and how many seats are free.

  2. create_user with a displayName and userPrincipalName. A strong temporary password is generated when you do not supply one, and returned in the result with forceChangePasswordNextSignIn set. Store it securely and share it out-of-band. create_user fails clearly if the UPN already exists.

  3. assign_license with a friendly name (E3), a skuPartNumber (ENTERPRISEPACK), or a SKU GUID. Pass usageLocation on the same call for a brand-new user.

  4. add_group_member to 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 usageLocation to assign_license, it is PATCHed onto the user before the license is assigned.

  • If you do not, the server reads the user's current usageLocation and returns a clear, actionable error when it is missing — rather than letting Graph fail cryptically.

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 matches client-side) and:

  • matches a GUID directly against skuId;

  • otherwise matches skuPartNumber case-insensitively;

  • otherwise maps a friendly name (e.g. E3ENTERPRISEPACK / SPE_E3, E5ENTERPRISEPREMIUM / SPE_E5) to the first candidate the tenant owns;

  • checks availability (prepaidUnits.enabled - consumedUnits) and refuses to assign when no seats are free.

Rate limiting

On HTTP 429, the raised GraphHttpError carries the Retry-After value and the Graph 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, with riskLevel and enabledByDefault so the gateway can gate writes. Read tools are enabledByDefault: true; every write tool is enabledByDefault: false.

Live smoke test

The normal test suite uses mocked fetch. With real credentials in the environment 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_URL must be https://; loopback http:// is allowed for local mocks.

  • Formatted errors redact Authorization headers, client secrets, access tokens, and token-like material.

  • Write tools are disabled by default and surfaced with write/destructive annotations.

  • The Streamable HTTP transport binds to 127.0.0.1 by default.

Security reports: security@borgels.com.

A
license - permissive license
-
quality - not tested
C
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

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

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/borgels/mcp-server-microsoft365'

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