Skip to main content
Glama
mcp-server-microsoft365

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

Privileged-target check (before password reset, TAP, group membership)

RoleManagement.Read.Directory + GroupMember.Read.All (or Directory.Read.All)

Read Intune devices (list_user_devices, list_managed_devices)

DeviceManagementManagedDevices.Read.All

Read Intune configuration / compliance policies

DeviceManagementConfiguration.Read.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"
      }
    }
  }
}

Intune (read)

list_user_devices, list_managed_devices, list_device_configurations and list_compliance_policies read Intune through Graph (/users/{id}/managedDevices, /deviceManagement/...). They need the application permissions DeviceManagementManagedDevices.Read.All and DeviceManagementConfiguration.Read.All. Pass includeAssignments: true to expand group assignments on policies.

Write gating

All write and destructive tools (create_user, assign_license, add_group_member, create_temporary_access_pass, ...) are only registered when MS_ENABLE_WRITES=true. The default is read-only, so a container started without the flag exposes the seven read tools and nothing else; callers see write tools as unknown rather than forbidden. Gating is by tool annotation (readOnlyHint), so a new write tool is off by default.

Account-takeover operations

Password reset and Temporary Access Pass hand a working credential to whoever called the tool, so they have their own opt-in on top of MS_ENABLE_WRITES=true:

Variable

Default

Effect

MS_ENABLE_PASSWORD_RESET

false

Registers reset_password (only exactly true enables it).

MS_ENABLE_TAP

false

Registers create_temporary_access_pass / delete_temporary_access_pass.

MS_PROTECTED_USERS

empty

Comma-separated object ids / UPNs / mail addresses that are always refused.

MS_PROTECTED_GROUPS

empty

Comma-separated group object ids / display names / mailNicknames that are always refused.

Before reset_password, create_temporary_access_pass, add_group_member and remove_group_member the server checks the target and refuses when:

  • the user holds a directory role (transitiveMemberOf contains a directoryRole) or has any entry in /roleManagement/directory/roleAssignments (including administrative-unit scoped roles);

  • the user is a member of a role-assignable group, or of a group in MS_PROTECTED_GROUPS;

  • the user matches MS_PROTECTED_USERS;

  • the group is role-assignable (isAssignableToRole=true) or is in MS_PROTECTED_GROUPS.

If any of these lookups fails (missing permission, throttling, unexpected response) the operation is refused: the check fails closed. Privileged accounts are meant to be handled by a human in the Entra portal. PIM-eligible (not active) role holders are not detected by these lookups; list such accounts in MS_PROTECTED_USERS.

Identifier validation

Identifiers that end up in a Graph URL path (user, group, method ids) must not be empty, . or .., and must not contain /, \, ?, # or percent-encoded ., / or \. Guest accounts whose UPN contains #EXT# must therefore be addressed by object id.

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 (compared in constant time), 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. E3 → ENTERPRISEPACK / SPE_E3, E5 → ENTERPRISEPREMIUM / 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.

Available Tools

18 tools
activate_pim_roleActivate PIM Role (self)A

Just-in-time PIM elevation: the delegated caller self-activates one of their own ELIGIBLE directory roles (e.g. Authentication Administrator, needed for Temporary Access Pass) for a bounded window. Requires a delegated token; fails if the caller is not eligible.

Inputs: roleDefinitionId (required), duration, justification, directoryScopeId.

add_group_memberAdd Group MemberA

Add a user to a group. Idempotent when the user is already a member.

Inputs: userId (required), groupId (required).

assign_licenseAssign License To UserA

Assign a license to a user. Sets usageLocation first when provided; fails clearly if the user has no usageLocation.

Inputs: user (required), license (required), disabledPlans, usageLocation.

create_temporary_access_passCreate Temporary Access PassA

Create a Temporary Access Pass (TAP) for passwordless first sign-in / MFA setup. Multi-use by default; regenerated until the passcode is alphanumeric so it is easy to relay. lifetimeInMinutes is bounded by the tenant TAP policy. Returns the passcode; deliver it out-of-band.

Inputs: user (required), maxAttempts, isUsableOnce, startDateTime, +2 more.

create_userCreate Microsoft 365 UserB

Create a user with a generated temporary password. Returns the temporary password; store it securely and share out-of-band.

Inputs: displayName (required), userPrincipalName (required), password, mailNickname, +4 more.

delete_temporary_access_passDelete Temporary Access PassA
DestructiveIdempotent

Delete a user's Temporary Access Pass. With methodId deletes that pass; otherwise deletes every TAP on the user.

Inputs: user (required), methodId.

get_groupGet Microsoft 365 GroupA
Read-onlyIdempotent

Fetch one group by object id.

Inputs: groupId (required), select.

get_userGet Microsoft 365 UserA
Read-onlyIdempotent

Fetch one user by object id or userPrincipalName.

Inputs: user (required), select.

get_user_license_detailsGet User License DetailsA
Read-onlyIdempotent

List the licenses currently assigned to a user.

Inputs: user (required).

list_group_membersList Group MembersC
Read-onlyIdempotent

List the members of a group.

Inputs: groupId (required), top.

list_groupsList Microsoft 365 GroupsC
Read-onlyIdempotent

List groups with optional $top, $filter, $select, and $orderby.

Inputs: top, filter, select, orderby.

list_subscribed_skusList Subscribed SKUsA
Read-onlyIdempotent

List tenant subscribed SKUs for license lookup and seat availability.

Inputs: no named parameters.

list_usersList Microsoft 365 UsersA
Read-onlyIdempotent

List users with optional $top, $filter, $select, and $orderby.

Inputs: top, filter, select, orderby.

remove_group_memberRemove Group MemberA
DestructiveIdempotent

Remove a user from a group. Idempotent when the user is not a member.

Inputs: userId (required), groupId (required).

remove_licenseRemove License From UserC
DestructiveIdempotent

Remove a license from a user.

Inputs: user (required), license (required).

set_managerSet User ManagerB

Set a user's manager. Provide the manager's object id or userPrincipalName.

Inputs: user (required), manager (required).

set_usage_locationSet User Usage LocationA

Set a user usageLocation (required before license assignment).

Inputs: user (required), usageLocation (required).

update_userUpdate Microsoft 365 UserA

Update attributes on an existing user (PATCH). Use accountEnabled to enable/disable an existing account (e.g. activate a pre-created hire or disable a leaver). Only provided fields are changed.

Inputs: user (required), city, state, country, +15 more.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    A3.7/5.0

    Scored across 18 tools

    Disambiguation5/5

    Each tool targets a specific resource and action, such as get_user vs list_users, assign_license vs remove_license, and create_temporary_access_pass vs delete_temporary_access_pass. There is no meaningful overlap that would cause an agent to select the wrong tool.

    Naming Consistency5/5

    All tool names follow a consistent verb_noun snake_case pattern (e.g., get_user, list_groups, assign_license, create_temporary_access_pass). No mixing of conventions or vague verbs.

    Tool Count4/5

    With 18 tools, the server is slightly above the typical 3-15 range but justifiably so, as it covers user/group management, licensing, temporary access passes, and PIM roles. The count feels appropriate for the broad Microsoft 365 domain without being bloated.

    Completeness4/5

    The tool surface covers core user lifecycle (create/get/update/list), license management, group membership, and authentication flows. The main gap is lack of user deletion, but this is workable via update_user disable, and group creation/deletion appears out of scope, making these minor gaps.

    Related MCP Connectors

    Related MCP Servers

    • -
      license
      Not graded
      quality
      Not graded
      maintenance
      Provides secure access to Microsoft Entra ID (Azure AD) resources including users, devices, and applications through Microsoft Graph API. Enables querying organizational data with comprehensive audit logging to Azure Blob Storage.
      -
    • A
      license
      B
      quality
      Not graded
      maintenance
      Provides comprehensive management of Microsoft 365 services including Exchange, SharePoint, Teams, Azure AD, Intune device management, security & compliance frameworks, and universal access to 1000+ Microsoft Graph API endpoints with advanced features like batch operations, delta queries, and real-time webhooks.
      50
      15
      -