Msgraphmcp
MCP Server for Microsoft Graph API with delegated permissions (device code flow, 115+ tools)
Ask AI about Msgraphmcp
Powered by Claude Β· Grounded in docs
I know everything about Msgraphmcp. Ask me about installation, configuration, usage, or troubleshooting.
0/500
Reviews
Documentation
msgraphmcp
MCP Server for the Microsoft Graph API β runs as a container, exposes 140+ tools across all major Microsoft 365 workloads to any MCP-compatible client such as Claude Code, and supports four authentication modes: authorization code + PKCE (delegated, recommended for Kubernetes/HTTP), client secret (app-only), client certificate (app-only, recommended for production), and device code (interactive, local use).
Table of Contents
- Architecture
- Prerequisites
- Azure App Registration
- Quick Start
- Environment Variables
- Authentication Flow
- Tool Reference
- Users
- Calendar
- OneDrive / Files
- Groups
- Microsoft Teams
- Contacts
- To Do / Tasks
- SharePoint Sites
- Intune β Apps
- Intune β Device Configurations
- Intune β Settings Catalog
- Intune β Compliance Policies
- Intune β Managed Devices
- Intune β Device Diagnostics
- Intune β Notification Templates
- Auth
- Development
- CI/CD and Docker Registry
- Security Notes
Architecture
Claude Code (MCP client) Another MCP client
β stdio / HTTP β HTTP
βΌ βΌ
msgraphmcp (Node.js / TypeScript)
βββ auth/TokenManager Four auth modes β per-session in HTTP mode
βββ graph/GraphClient Axios wrapper, user-stamped logs, auto-pagination, single retry on 401
βββ tools/
βββ users Β· mail Β· calendar Β· files Β· groups
βββ teams Β· contacts Β· tasks Β· sites
βββ intune (apps Β· device config Β· settings catalog Β· compliance Β· managed devices)
β HTTPS
βΌ
Microsoft Graph API (https://graph.microsoft.com/v1.0)
HTTP mode auth flow (authorization code, per-session):
1. MCP client connects β server creates session with isolated TokenManager
2. Tool call without auth β 401 + loginUrl (/auth/login?token=<one-time-token>)
3. Browser β GET /auth/login?token=<one-time-token> β Microsoft Login
4. GET /auth/callback β token stored in that session's TokenManager only
5. Subsequent tool calls from the same session use that user's token exclusively
The login token is a single-use 256-bit value that maps server-side to the
MCP session β the session ID itself never appears in any URL, browser
history, or proxy log.
In HTTP mode every MCP session gets its own isolated TokenManager β tokens are never shared between sessions. Each user authenticates independently. Tokens are kept in-memory for the lifetime of the session; no cross-user token bleed is possible even when multiple clients are connected simultaneously.
Prerequisites
| Requirement | Version |
|---|---|
| Node.js | β₯ 20 |
| npm | β₯ 10 |
| Docker | β₯ 24 (optional) |
| Microsoft 365 / Azure AD tenant | β |
| Azure App Registration | see below |
Azure App Registration
-
Open Azure Portal β App registrations β New registration.
-
Name:
msgraphmcp(or any name) -
Supported account types: Accounts in this organizational directory only (or multitenant if needed)
-
Redirect URI β depends on the auth mode you plan to use:
- Authorization code flow (Mode A): select Web β enter your callback URL, e.g.
https://msgraph.example.com/auth/callback - Device code flow (Mode D): select Mobile and desktop applications β
https://login.microsoftonline.com/common/oauth2/nativeclient - App-only (Modes B/C): no redirect URI needed
- Authorization code flow (Mode A): select Web β enter your callback URL, e.g.
-
Click Register β copy the Application (client) ID and Directory (tenant) ID.
-
Add permissions β the type depends on the auth mode you choose:
- Authorization code / device code (delegated): Go to API Permissions β Add a permission β Microsoft Graph β Delegated permissions and add the permissions below, then click Grant admin consent.
- Client secret / certificate (app-only): Go to API Permissions β Add a permission β Microsoft Graph β Application permissions and add the same permissions, then click Grant admin consent.
With app-only auth,
userId: "me"does not resolve β use explicit UPNs or object IDs in all tool calls.
| Permission | Purpose |
|---|---|
User.ReadWrite.All | Manage users |
Group.ReadWrite.All | Manage groups |
GroupMember.ReadWrite.All | Manage group membership |
Mail.ReadWrite | Read/write mailboxes |
Mail.Send | Send email |
Calendars.ReadWrite | Manage calendars & events |
Files.ReadWrite.All | OneDrive CRUD |
Sites.ReadWrite.All | SharePoint CRUD |
Tasks.ReadWrite | Microsoft To Do |
Contacts.ReadWrite | Contacts |
Team.ReadWrite.All | Teams management |
Channel.ReadWrite.All | Teams channels |
ChannelMessage.Send | Send Teams messages |
Directory.ReadWrite.All | Directory objects |
DeviceManagementApps.ReadWrite.All | Intune apps |
DeviceManagementConfiguration.ReadWrite.All | Intune device configs |
DeviceManagementManagedDevices.ReadWrite.All | Managed devices |
DeviceManagementServiceConfig.ReadWrite.All | Intune service config |
- Click Grant admin consent for <your tenant>.
Tip: You can restrict the scope by setting the
GRAPH_SCOPESenvironment variable to only the permissions you actually need.
Quick Start
Local (Node.js)
git clone https://github.com/DustHoff/msgraphmcp.git
cd msgraphmcp
npm install
npm run build
export AZURE_CLIENT_ID="your-client-id"
export AZURE_TENANT_ID="your-tenant-id"
export TOKEN_CACHE_PATH="$HOME/.msgraphmcp/tokens.json"
node dist/index.js
On first run the device code authentication prompt appears on stderr:
============================================================
To sign in, use a web browser to open the page
https://microsoft.com/devicelogin and enter the code XXXXXXXX to authenticate.
============================================================
After successful authentication the MCP server is ready and keeps the token refreshed automatically.
Docker
cp .env.example .env
# Edit .env with your AZURE_CLIENT_ID and AZURE_TENANT_ID
docker-compose up
docker-compose.yml mounts a named volume (token-cache) at /data so the token cache survives container restarts.
First run (one-time device code):
docker-compose run --rm msgraphmcp
# Follow the authentication prompt on screen, then Ctrl+C
# Subsequent runs will use the cached refresh token
docker-compose up -d
Claude Code Integration
Add the server to your project's .claude/settings.json:
{
"mcpServers": {
"msgraphmcp": {
"command": "node",
"args": ["dist/index.js"],
"env": {
"AZURE_CLIENT_ID": "your-client-id",
"AZURE_TENANT_ID": "your-tenant-id",
"TOKEN_CACHE_PATH": "/path/to/tokens.json"
}
}
}
}
Or using the pre-built Docker image from GHCR:
{
"mcpServers": {
"msgraphmcp": {
"command": "docker",
"args": [
"run", "--rm", "-i",
"-v", "msgraphmcp-tokens:/data",
"-e", "AZURE_CLIENT_ID=your-client-id",
"-e", "AZURE_TENANT_ID=your-tenant-id",
"ghcr.io/DustHoff/msgraphmcp:latest"
]
}
}
}
Restart Claude Code after editing the config β the server appears in the MCP tools panel.
Environment Variables
| Variable | Required | Default | Description |
|---|---|---|---|
AZURE_CLIENT_ID | Yes | β | App Registration client ID |
AZURE_TENANT_ID | No | common | Tenant ID or common for multi-tenant |
AZURE_CLIENT_SECRET | No | β | Client secret β required for Mode A (auth code) and Mode B (app-only) |
AZURE_REDIRECT_URI | No | β | Auth mode A β full callback URL, e.g. https://msgraph.example.com/auth/callback. When set together with AZURE_CLIENT_SECRET, activates the authorization code flow |
AZURE_CLIENT_CERTIFICATE_PATH | No | β | Auth mode C β path to a PEM private key file; must be set together with THUMBPRINT |
AZURE_CLIENT_CERTIFICATE_THUMBPRINT | No | β | Auth mode C β SHA-256 certificate thumbprint (64 hex chars) from the App Registration |
GRAPH_SCOPES | No | all scopes | Space-separated delegated scopes (used in auth code and device code modes) |
TOKEN_CACHE_PATH | No | /data/tokens.json | Path to the MSAL token cache file |
PORT | No | β | When set, the server listens on HTTP (Kubernetes mode); otherwise uses stdio |
LOG_LEVEL | No | info | Log verbosity: debug, info, warn, error |
MAX_SESSIONS | No | 50 | Maximum concurrent MCP sessions in HTTP mode. New connections beyond this limit receive HTTP 503. |
SESSION_IDLE_TIMEOUT_MINUTES | No | 60 | Minutes of inactivity before an MCP session is automatically closed and removed. |
Authentication Flow
Four modes are selected automatically by which environment variables are set:
| Mode | Env vars set | Type | User context |
|---|---|---|---|
| A β Authorization Code | AZURE_CLIENT_SECRET + AZURE_REDIRECT_URI | Delegated | Yes β full userId: "me" support |
| B β Client Secret | AZURE_CLIENT_SECRET (no redirect URI) | App-only | No β use explicit UPNs/object IDs |
| C β Client Certificate | AZURE_CLIENT_CERTIFICATE_PATH + THUMBPRINT | App-only | No |
| D β Device Code | none of the above | Delegated | Yes |
Mode A β Authorization Code + PKCE (delegated, recommended for HTTP/Kubernetes)
Set AZURE_CLIENT_SECRET and AZURE_REDIRECT_URI. Each MCP session authenticates independently β tokens are isolated per session and kept in-memory. Suitable for containers with Entra ID Conditional Access β CA compliance is evaluated against the user's browser device, not the container. Multiple users can be authenticated simultaneously.
Prerequisites:
- Register
AZURE_REDIRECT_URI(e.g.https://msgraph.example.com/auth/callback) as a Web redirect URI in the Entra ID app registration. - Grant Delegated permissions (not Application) + admin consent.
Authentication flow
Step 1 β Connect your MCP client (Claude Code or other)
POST /mcp (initialize)
βββΊ Server creates a session with its own isolated TokenManager
βββΊ Session ID returned in response header: mcp-session-id: <uuid>
Step 2 β Authenticate (once per session / pod restart)
Tool call without auth β 401 response containing a fresh loginUrl
GET /auth/login?token=<one-time-token>
βββΊ Server resolves the one-time token β session, invalidates the token
βββΊ Server generates PKCE code_verifier + S256 challenge
βββΊ Redirects browser β Microsoft login page
βββΊ User authenticates + consents
βββΊ Microsoft redirects β GET /auth/callback?code=...&state=...
βββΊ Server exchanges code and stores tokens in that session's TokenManager only
βββΊ Green success page shown β browser can be closed
Step 3 β Tool calls
βββΊ acquireTokenSilent() β uses the session's in-memory refresh token
βββΊ On 401 from Graph: single retry with fresh token, then error
How to start the login flow
A fresh loginUrl is embedded in every 401 response to an unauthenticated
tool call, and is also returned by the get_login_url MCP tool:
{
"error": "Unauthorized",
"loginUrl": "https://msgraph.example.com/auth/login?token=3941f8c7dca11388..."
}
Open the loginUrl directly in a browser to authenticate. The token is
single-use and expires after 15 minutes β each tool call / get_login_url
invocation mints a new one bound to the current MCP session. The session
ID itself is never exposed in URLs.
Health check
The /health endpoint intentionally returns only aggregate counts β no
session IDs, no UPNs β so that anyone who can reach the endpoint cannot
enumerate active users or sessions.
GET /health
β {
"status": "ok",
"service": "msgraphmcp",
"authMode": "authorization-code",
"sessions": 2,
"authenticatedSessions": 1
}
Token lifetime
Tokens are kept in-memory for the session's lifetime. They survive within a running pod but are lost on pod restart β each session must re-authenticate after a restart. The loginUrl in the 401 response makes re-authentication a single browser visit.
Kubernetes deployment
Uncomment the Option A block in k8s/deployment.yaml and set AZURE_REDIRECT_URI to your ingress hostname:
- name: AZURE_CLIENT_SECRET
valueFrom:
secretKeyRef:
name: msgraphmcp-azure
key: AZURE_CLIENT_SECRET
- name: AZURE_REDIRECT_URI
value: https://msgraph.example.com/auth/callback
Mode B β Client Secret (app-only)
Set AZURE_CLIENT_SECRET without AZURE_REDIRECT_URI. Requires Application permissions + admin consent. Recommended for fully automated deployments where no user context is needed.
Every request
βββΊ acquireTokenByClientCredential({ scopes: ['.default'] })
βββΊ Entra ID returns a short-lived access token (no refresh token stored)
βββΊ Device compliance CA policies are NOT evaluated β safe in containers
Mode C β Client Certificate (app-only, recommended for production)
Set AZURE_CLIENT_CERTIFICATE_PATH + AZURE_CLIENT_CERTIFICATE_THUMBPRINT. Same flow as Mode B but uses a certificate assertion instead of a shared secret β no secret rotation required.
Every request
βββΊ acquireTokenByClientCredential with cert assertion
βββΊ Entra ID returns a short-lived access token
βββΊ Device compliance CA policies are NOT evaluated
Kubernetes setup:
# 1. Generate a self-signed key + cert (or use your PKI)
openssl req -x509 -newkey rsa:2048 -keyout tls.key -out tls.crt -days 365 -nodes -subj "/CN=msgraphmcp"
# 2. Get the SHA-256 thumbprint (64 hex chars)
openssl x509 -in tls.crt -fingerprint -sha256 -noout | tr -d ':' | sed 's/.*=//'
# 3. Upload tls.crt to the App Registration β Certificates & secrets β Certificates
# 4. Store the private key as a Kubernetes Secret
kubectl create secret generic msgraphmcp-client-cert --from-file=tls.key -n msgraphmcp
Then uncomment the client-cert volume in k8s/deployment.yaml and set AZURE_CLIENT_CERTIFICATE_THUMBPRINT in k8s/secret.yaml.
Mode D β Device Code (delegated, local / interactive)
No secret or certificate configured. Suitable for local use and Claude Code stdio integration. Not recommended in containers under Entra ID Conditional Access device-compliance policies β token refresh from a non-enrolled host will be rejected.
First run
βββΊ Device code prompt (stderr) β user visits URL, enters code
βββΊ MSAL receives tokens β writes cache to TOKEN_CACHE_PATH
Subsequent runs / token expiry
βββΊ acquireTokenSilent() uses cached refresh token
βββΊ CA compliance evaluated against the container β may fail
401 from Graph API
βββΊ Interceptor retries once with fresh token, then throws error
The refresh token typically lasts 90 days. If it expires, the device code prompt appears again on next start.
Tool Reference
All tools accept userId: string parameters that default to "me" (the signed-in user) unless noted.
Parameters marked optional can be omitted.
All ids used in examples below (
00000000-0000-0000-0000-...,grp-id, β¦) are placeholders β never substitute real tenant, user, group or app ids into the documentation.
Users
| Tool | Description | Key Parameters |
|---|---|---|
list_users | List directory users | filter, select, top, search |
get_user | Get a single user | userId (required), select |
create_user | Create a new user | displayName, userPrincipalName, mailNickname, password |
update_user | Update user properties | userId, displayName, jobTitle, department, β¦ |
delete_user | Delete a user | userId |
get_user_member_of | Get groups/roles the user belongs to | userId |
reset_user_password | Reset a user's password | userId, newPassword, forceChangePasswordNextSignIn |
Example β create user:
create_user displayName="Alice MΓΌller" userPrincipalName="alice@contoso.com" mailNickname="alice" password="P@ssw0rd!"
| Tool | Description | Key Parameters |
|---|---|---|
list_messages | List messages in a folder | userId, folderId (default inbox), filter, top, search |
get_message | Get a specific message | userId, messageId |
send_mail | Send an email | subject, body, toRecipients[], ccRecipients[], bccRecipients[] |
reply_to_message | Reply to a message | userId, messageId, comment |
forward_message | Forward a message | userId, messageId, toRecipients[], comment |
delete_message | Delete a message | userId, messageId |
move_message | Move to another folder | userId, messageId, destinationFolderId |
list_mail_folders | List mail folders | userId, includeHiddenFolders |
create_mail_folder | Create a new folder | userId, displayName, parentFolderId |
Well-known folder IDs: inbox, sentitems, drafts, deleteditems, archive, junkemail
Example β send mail:
send_mail subject="Meeting tomorrow" body="Hi Bob,\nSee you at 10:00." toRecipients=[{address:"bob@contoso.com"}]
Calendar
| Tool | Description | Key Parameters |
|---|---|---|
list_calendars | List all calendars | userId |
create_calendar | Create a calendar | userId, name, color |
list_events | List events (or calendar view) | userId, calendarId, startDateTime, endDateTime, filter, top |
get_event | Get a specific event | userId, eventId |
create_event | Create an event | subject, startDateTime, endDateTime, attendees[], location, isOnlineMeeting |
update_event | Update an event | userId, eventId, + any field |
delete_event | Delete an event | userId, eventId |
Example β create Teams meeting:
create_event subject="Sprint Review" startDateTime="2024-06-14T14:00:00" endDateTime="2024-06-14T15:00:00" isOnlineMeeting=true attendees=[{address:"bob@contoso.com",type:"required"}]
OneDrive / Files
| Tool | Description | Key Parameters |
|---|---|---|
list_drive_items | List items in a folder | userId, itemPath (default /), top |
get_drive_item | Get metadata | userId, itemPath or itemId |
create_drive_folder | Create a folder | userId, parentPath, folderName, conflictBehavior |
upload_drive_file | Upload text file (β€ 4 MB) | userId, filePath, content, conflictBehavior |
delete_drive_item | Delete an item | userId, itemPath or itemId |
copy_drive_item | Copy an item | userId, itemId, destinationParentId, newName |
search_drive | Search OneDrive | userId, query, top |
list_shared_with_me | List shared items | userId |
Groups
| Tool | Description | Key Parameters |
|---|---|---|
list_groups | List groups | filter, select, top, search |
get_group | Get a group | groupId, select |
create_group | Create M365 or Security group | displayName, mailNickname, groupType (Microsoft365|Security) |
update_group | Update group | groupId, displayName, description, visibility |
delete_group | Delete a group | groupId |
list_group_members | List members | groupId, select |
add_group_member | Add a member | groupId, memberId |
remove_group_member | Remove a member | groupId, memberId |
list_group_owners | List owners | groupId |
add_group_owner | Add an owner | groupId, ownerId |
Microsoft Teams
| Tool | Description | Key Parameters |
|---|---|---|
list_joined_teams | List teams the user belongs to | userId |
get_team | Get team details | teamId |
create_team | Create a new team | displayName, description, visibility, template |
list_channels | List channels | teamId |
get_channel | Get a channel | teamId, channelId |
create_channel | Create a channel | teamId, displayName, membershipType |
delete_channel | Delete a channel | teamId, channelId |
list_channel_messages | List messages | teamId, channelId, top |
send_channel_message | Post a message | teamId, channelId, content, contentType |
reply_to_channel_message | Reply to a message | teamId, channelId, messageId, content |
list_team_members | List members | teamId |
add_team_member | Add a member | teamId, userId, roles |
Contacts
| Tool | Description | Key Parameters |
|---|---|---|
list_contacts | List contacts | userId, filter, select, top |
get_contact | Get a contact | userId, contactId |
create_contact | Create a contact | userId, givenName, surname, emailAddresses[], businessPhones[] |
update_contact | Update a contact | userId, contactId, + any field |
delete_contact | Delete a contact | userId, contactId |
To Do / Tasks
| Tool | Description | Key Parameters |
|---|---|---|
list_todo_lists | List task lists | userId |
create_todo_list | Create a task list | userId, displayName |
delete_todo_list | Delete a task list | userId, listId |
list_tasks | List tasks in a list | userId, listId, filter, top |
create_task | Create a task | userId, listId, title, dueDateTime, importance, reminderDateTime |
update_task | Update a task | userId, listId, taskId, status, title, importance |
complete_task | Mark as completed | userId, listId, taskId |
delete_task | Delete a task | userId, listId, taskId |
Task status values: notStarted, inProgress, completed, waitingOnOthers, deferred
SharePoint Sites
| Tool | Description | Key Parameters |
|---|---|---|
list_sites | List sites | filter, top |
get_site | Get a site | siteId or (hostname + sitePath) |
search_sites | Search by keyword | query |
list_site_lists | List site lists/libraries | siteId |
get_site_list | Get a list | siteId, listId |
list_site_list_items | List list items | siteId, listId, filter, top, expand |
get_site_list_item | Get an item (with fields) | siteId, listId, itemId |
create_site_list_item | Create an item | siteId, listId, fields (key-value object) |
update_site_list_item | Update item fields | siteId, listId, itemId, fields |
delete_site_list_item | Delete an item | siteId, listId, itemId |
Intune β Apps
| Tool | Description | Key Parameters |
|---|---|---|
list_intune_apps | List managed apps | filter, appType, select, top |
get_intune_app | Get app details | appId, select |
create_intune_web_app | Add a web shortcut app | displayName, publisher, appUrl |
create_intune_store_app | Add a store app | displayName, publisher, storeType (windowsStore|iosStore|androidStore), appStoreUrl |
update_intune_app | Update app metadata (common mobileApp fields + Win32-specific). Win32 fields auto-mark the body as #microsoft.graph.win32LobApp. | appId, displayName, publisher, description, isFeatured, privacyInformationUrl, informationUrl, notes, owner, developer, roleScopeTagIds[], largeIcon, installCommandLine, uninstallCommandLine, setupFilePath, applicableArchitectures, allowedArchitectures, minimumSupportedWindowsRelease, minimumFreeDiskSpaceInMB, minimumMemoryInMB, minimumNumberOfProcessors, minimumCpuSpeedInMHz, displayVersion, installExperience, returnCodes[], rules[] |
delete_intune_app | Delete an app | appId |
upload_win32_lob_app | Upload a .intunewin Win32 LOB package. Source is exactly one of: (a) filePath, (b) fileUrl, or (c) a OneDrive reference (oneDriveItemPath or oneDriveItemId, optionally with oneDriveUserId β defaults to "me"). | filePath | fileUrl | (oneDriveItemPath | oneDriveItemId [+ oneDriveUserId]), displayName, publisher, description, installCommandLine, uninstallCommandLine, setupFilePath, applicableArchitectures, minimumSupportedWindowsRelease, runAsAccount, deviceRestartBehavior |
update_windows_msix_app_content | Replace the package content of an existing #microsoft.graph.windowsUniversalAppX (MSIX) app. Creates a new contentVersion, uploads the .msix unencrypted to the Intune-allocated Azure Blob, commits it, and patches committedContentVersion. Verifies the target app's @odata.type first β Win32, web and store apps are rejected. | appId, filePath | fileUrl | (oneDriveItemPath | oneDriveItemId [+ oneDriveUserId]), fileName (optional) |
list_intune_app_relationships | List supersedence / dependency links | appId |
set_intune_app_relationships | Set supersedence / dependency links (replaces all) | appId, relationships[] |
list_intune_app_assignments | List assignments | appId |
assign_intune_app | Assign to groups (replaces all existing) | appId, assignments[] (groupId, intent, filterMode, filterId) |
get_intune_app_install_status | Per-device install status (Reports API) | appId, top, includeUserStatuses |
list_intune_app_protection_policies | List MAM / App Protection policies | platform (ios|android|all) |
Assignment intent values: available, required, uninstall, availableWithoutEnrollment
Win32 LOB upload notes:
- Provide exactly one source:
filePath(server-side absolute path),fileUrl(HTTP/HTTPS URL) or a OneDrive reference (oneDriveItemPathoroneDriveItemId, optionally withoneDriveUserIdβ defaults to"me"). fileUrlis fetched server-side with an SSRF guard (http/https only; loopback, link-local, RFC1918 ranges, and cloud-metadata hostnames are blocked).- OneDrive sources resolve the DriveItem via Graph (
$select=id,name,size,file,folder,@microsoft.graph.downloadUrl), reject folders, and stream the short-lived pre-authenticated download URL through the same bounded writer. - Downloads are capped at 2 GB (applies to
fileUrland OneDrive sources alike). - After upload, set detection/requirement rules via
update_intune_app(fieldrules[]) and assign to groups viaassign_intune_app.
Example β upload from OneDrive:
upload_win32_lob_app
oneDriveItemPath="/Apps/myapp.intunewin"
displayName="My App"
publisher="Contoso"
installCommandLine="setup.exe /S"
uninstallCommandLine="setup.exe /U"
setupFilePath="setup.exe"
MSIX content update notes:
update_windows_msix_app_contentrequires the target app to be a#microsoft.graph.windowsUniversalAppX; other types are rejected up-front so you don't create an orphaned contentVersion.- MSIX payloads are uploaded unencrypted β the commit body is
{}, nofileEncryptionInfois sent. - OneDrive and
fileUrlsources reuse the same 2 GB cap and SSRF guard as the Win32 flow. - Assignments on the app are preserved β only the committed content version changes.
Example β update an existing MSIX app from OneDrive:
update_windows_msix_app_content
appId="00000000-0000-0000-0000-000000000000"
oneDriveItemPath="/intune/myapp/myapp.msix"
Example β assign app as required:
assign_intune_app appId="00000000-0000-0000-0000-000000000001" assignments=[{groupId:"grp-id",intent:"required"}]
Intune β Device Configurations
| Tool | Description | Key Parameters |
|---|---|---|
list_device_configurations | List config profiles | filter, select, top |
get_device_configuration | Get a profile | configId |
create_device_configuration | Create a profile | displayName, odataType, settings (key-value) |
update_device_configuration | Update a profile | configId, displayName, settings |
delete_device_configuration | Delete a profile | configId |
assign_device_configuration | Assign to groups | configId, assignments[] (groupId, intent: include|exclude) |
get_device_configuration_assignments | List assignments | configId |
get_device_configuration_device_status | Per-device status | configId, top |
Common odataType values:
| Platform | @odata.type |
|---|---|
| Windows 10 (general) | #microsoft.graph.windows10GeneralConfiguration |
| Windows 10 (endpoint protection) | #microsoft.graph.windows10EndpointProtectionConfiguration |
| iOS | #microsoft.graph.iosGeneralDeviceConfiguration |
| Android | #microsoft.graph.androidGeneralDeviceConfiguration |
| macOS | #microsoft.graph.macOSGeneralDeviceConfiguration |
Example β create Windows 10 password policy:
create_device_configuration
displayName="Windows Password Policy"
odataType="#microsoft.graph.windows10GeneralConfiguration"
settings={"passwordRequired":true,"passwordMinimumLength":12,"passwordRequiredType":"alphanumeric"}
Intune β Settings Catalog
The Settings Catalog is the modern replacement for device configuration profiles and supports a broader set of settings.
| Tool | Description | Key Parameters |
|---|---|---|
list_configuration_policies | List catalog policies | filter, top |
get_configuration_policy | Get policy + settings | policyId |
create_configuration_policy | Create a policy | name, platforms, technologies, settings[] |
update_configuration_policy | Update name/description | policyId, name, description |
delete_configuration_policy | Delete a policy | policyId |
assign_configuration_policy | Assign to groups | policyId, assignments[] |
search_settings_catalog | Find available settings | keyword, platform, top |
Workflow β create a BitLocker policy:
1. search_settings_catalog keyword="BitLocker" platform="windows10"
β note the settingDefinitionId values
2. create_configuration_policy
name="BitLocker Encryption"
platforms="windows10"
technologies="mdm"
settings=[{
id:"0",
settingInstance:{
"@odata.type":"#microsoft.graph.deviceManagementConfigurationChoiceSettingInstance",
"settingDefinitionId":"device_vendor_msft_bitlocker_requiredeviceencryption",
"choiceSettingValue":{"value":"device_vendor_msft_bitlocker_requiredeviceencryption_1","children":[]}
}
}]
Intune β Compliance Policies
| Tool | Description | Key Parameters |
|---|---|---|
list_compliance_policies | List compliance policies | filter, top |
get_compliance_policy | Get a policy | policyId |
create_compliance_policy | Create a policy | displayName, odataType, settings, scheduledActionsForRule |
update_compliance_policy | Update a policy | policyId, displayName, settings |
delete_compliance_policy | Delete a policy | policyId |
assign_compliance_policy | Assign to groups | policyId, assignments[] |
get_compliance_policy_device_status | Per-device status | policyId, top |
Common odataType values:
| Platform | @odata.type |
|---|---|
| Windows 10 | #microsoft.graph.windows10CompliancePolicy |
| iOS | #microsoft.graph.iosCompliancePolicy |
| Android | #microsoft.graph.androidCompliancePolicy |
| macOS | #microsoft.graph.macOSCompliancePolicy |
Non-compliance actions: block, retire, wipe, notification, pushNotification
Intune β Managed Devices
| Tool | Description | Key Parameters |
|---|---|---|
list_managed_devices | List enrolled devices | filter, select, top |
get_managed_device | Get device details | deviceId |
sync_managed_device | Trigger policy sync | deviceId |
restart_managed_device | Reboot a device | deviceId |
shutdown_managed_device | Shut down a Windows device | deviceId |
lock_managed_device | Remote lock (PIN/password required to unlock) | deviceId |
set_managed_device_name | Rename a Windows device (β€ 15 chars) | deviceId, deviceName |
retire_managed_device | Unenroll (keep personal data) | deviceId |
wipe_managed_device | Factory reset (destructive) | deviceId, keepEnrollmentData, keepUserData |
disable_managed_device | Disable a device β keeps enrollment, blocks access | deviceId |
reenable_managed_device | Re-enable a previously disabled device | deviceId |
send_device_notification | Push a Company Portal notification | deviceId, notificationTitle, notificationBody |
windows_defender_scan | Trigger a Defender antivirus scan | deviceId, quickScan |
windows_defender_update_signatures | Force Defender signature update | deviceId |
rotate_bitlocker_keys | Rotate BitLocker recovery key (escrowed to Entra) | deviceId |
rotate_local_admin_password | Rotate Windows LAPS local admin password | deviceId |
trigger_proactive_remediation | Run a Proactive Remediation script on demand | deviceId, scriptId |
get_device_compliance_overview | Tenant-wide compliance stats | β |
Warning:
wipe_managed_deviceerases all data on the device. Use with extreme caution.
Useful filter examples for list_managed_devices:
filter="operatingSystem eq 'Windows'"
filter="complianceState eq 'noncompliant'"
filter="contains(deviceName,'DESKTOP')"
Intune β Device Diagnostics
| Tool | Description | Key Parameters |
|---|---|---|
collect_device_diagnostics | Start a "Collect diagnostics" remote action | deviceId |
list_device_diagnostics | List / get status of diagnostic collection requests | deviceId, requestId (optional) |
download_device_diagnostics | Generate a SAS download URL for a completed ZIP | deviceId, requestId |
Workflow:
collect_device_diagnosticsβ returns arequestId.- Poll
list_device_diagnosticsuntilstatus: "completed". download_device_diagnosticsβ returns{ value: "<sas-url>" }pointing at the ZIP archive.
Intune β Notification Templates
| Tool | Description | Key Parameters |
|---|---|---|
list_notification_templates | List templates | top |
get_notification_template | Get a template + localized messages | templateId |
create_notification_template | Create a template (via beta) | displayName, defaultLocale, brandingOptions, roleScopeTagIds |
update_notification_template | Update template metadata | templateId, displayName, brandingOptions, β¦ |
delete_notification_template | Delete a template | templateId |
add_notification_template_message | Add / update a localized message | templateId, locale, subject, messageTemplate, isDefault |
send_notification_template_test | Send a test email using the default locale | templateId |
create_notification_templateis routed through thebetaAPI because the v1.0 endpoint returns 400 for all write operations on this resource.
Auth
| Tool | Description | Key Parameters |
|---|---|---|
get_login_url | Returns authentication status and a fresh one-time login URL when sign-in is required. Call this first whenever another tool returns an auth error. | β |
Development
# Install dependencies
npm install
# Start in watch mode (ts-node, no compile step)
npm run dev
# Build production bundle (esbuild, ~10 ms)
npm run build
# Run unit tests
npm test
# Run tests with coverage report
npm run test:coverage
# TypeScript type check only (no emit)
npm run typecheck
Project Structure
msgraphmcp/
βββ src/
β βββ index.ts Entry point β creates MCP server, connects stdio transport
β βββ auth/
β β βββ TokenManager.ts MSAL public client, device code flow, silent refresh
β βββ graph/
β β βββ GraphClient.ts Axios wrapper: auth header injection, pagination, error handling
β βββ tools/
β βββ shared.ts URL / OData / HTML escaping helpers
β βββ auth.ts 1 tool (get_login_url)
β βββ users.ts 7 tools
β βββ mail.ts 9 tools
β βββ calendar.ts 7 tools
β βββ files.ts 8 tools
β βββ groups.ts 10 tools
β βββ teams.ts 12 tools
β βββ contacts.ts 5 tools
β βββ tasks.ts 8 tools
β βββ sites.ts 10 tools
β βββ intune.ts 64 tools (apps Β· win32 LOB Β· MSIX Β· config Β· settings catalog Β· compliance Β· devices Β· diagnostics Β· notification templates)
βββ tests/
β βββ helpers/
β β βββ MockMcpServer.ts Captures tool registrations for unit testing
β β βββ mockGraphClient.ts Jest mock for GraphClient
β βββ auth/
β β βββ TokenManager.test.ts
β βββ graph/
β β βββ GraphClient.test.ts
β βββ tools/
β βββ users.test.ts β¦ intune.test.ts
βββ .github/
β βββ workflows/
β βββ ci.yml Test + build on push/PR
β βββ docker.yml Build multi-arch image β ghcr.io
βββ Dockerfile
βββ docker-compose.yml
βββ .env.example
CI/CD and Docker Registry
GitHub Actions
| Workflow | Trigger | Jobs |
|---|---|---|
ci.yml | push to main/develop, PR to main | typecheck β test (with coverage) β build β smoke test |
docker.yml | push to main, version tags v*.*.*, manual | test (gate) β build multi-arch image β push to GHCR β provenance attestation |
Docker image tags
| Tag | When created |
|---|---|
latest | Every push to main |
1.2.3 | When a v1.2.3 tag is pushed |
1.2 | When a v1.2.x tag is pushed |
sha-abc1234 | Every push (traceability) |
Pulling from GHCR
docker pull ghcr.io/DustHoff/msgraphmcp:latest
Setup in your repository
- Replace
DustHoffin the badge URLs above with your GitHub username or organisation. - Push to a GitHub repository β Actions will run automatically.
- For private packages: go to Settings β Packages β msgraphmcp β Package settings β Change visibility if needed.
Security Notes
- Per-session token isolation (Mode A): each MCP session in HTTP mode holds its own in-memory token. Tokens are never shared between sessions β a compromised session cannot access another user's data. Tokens are lost on pod restart; re-authentication is a single browser visit via the
loginUrlin the 401 response. - One-time login tokens: the
loginUrlcarries a single-use 256-bit token (not the MCP session ID). It is consumed on first visit, expires after 15 minutes, and binds server-side to exactly one session β so even if a login URL is disclosed via browser history, a proxy log, or an email it cannot be replayed. - HTML escaping on the auth pages: the
/auth/callbackerror page escapes all five HTML-significant characters (&,<,>,",') β an attacker cannot reflect a craftederror_description(including HTML numeric entities like<script>) into executable markup. - URL-path safety: all user-supplied opaque ids passed to Graph (
groupId,teamId,channelId,appId,configId,policyId,templateId,deviceId,siteId,listId,itemId,memberId,ownerId,userId) are percent-encoded before they are embedded in Graph URLs, so a tool argument cannot smuggle extra path segments, query strings, or fragments into a request. SharePoint composite site ids (hostname,guid,guid) keep their commas intact. - SSRF guard on Win32 LOB upload:
upload_win32_lob_appwith afileUrlargument rejects non-http(s) schemes and blocks loopback, link-local, RFC1918, cloud-metadata and multicast ranges before issuing the download. OneDrive sources reuse the same guard against the short-lived pre-authenticated download URL returned by Graph. The download size is capped at 2 GB in all cases. - Request-body limit: the HTTP server caps incoming request bodies at 4 MB and destroys the socket immediately on overflow to prevent OOM via a large JSON-RPC payload.
- Session limits:
MAX_SESSIONS(default 50) caps concurrent sessions;SESSION_IDLE_TIMEOUT_MINUTES(default 60) reaps idle sessions so abandoned connections do not leak memory. - Sensitive-value redaction in debug logs: values of keys matching
/password|secret|token|credential|private[-_]?key|apikey/iare replaced with***REDACTED***before any request/response body is logged. - Token cache file (
tokens.json) is only written in device-code (Mode D) and stdio mode. Written withmode 0o600; mount as a restricted Docker volume; do not bake into images. - The image runs as the non-root
nodeuser (seeDockerfile). - Secrets (
AZURE_CLIENT_ID,AZURE_CLIENT_SECRET, certificate thumbprint) β use Kubernetes Secrets or an external vault; never commit real values to the repository.secrets.txt,tokens.json, anddata/are excluded via both.gitignoreand.dockerignore. - Prefer authorization code (Mode A) over device code (Mode D) for HTTP deployments β tokens are tied to the user's browser device so CA compliance is evaluated correctly. Device code refresh from a non-enrolled container is rejected by device-compliance CA policies.
- Prefer client certificate (Mode C) over client secret (Mode B) for app-only deployments β certificates are not transmitted over the wire and can be rotated without application downtime.
- Authorization code flow:
/auth/login?token=<one-time-token>and/auth/callbackmust be reachable by the authenticating browser but do not need to be internet-facing β internal DNS is sufficient. Requests without a valid, unexpired token are rejected with 400. - Graph API logging includes the authenticated user's UPN (
"user": "alice@contoso.com") in every log entry, making all MS Graph calls attributable to a specific user identity. SetLOG_LEVEL=warnin production to suppress URL logging if UPNs in logs are a concern. - Scope down
GRAPH_SCOPES(delegated modes) or grant only the required permissions (app-only modes) for your use case. wipe_managed_deviceis irreversible β consider requiring explicit confirmation in your workflows.- See
SECURITY-NOTICE.mdfor the full security assessment including dependency risk analysis.
