entraadm-mcp
English | 日本語
MCP server for Microsoft Entra ID sign-in and audit-log triage, plus the two Azure-side questions a morning patrol asks with the same credential: are our Azure VMs backed up, and is Microsoft 365 itself degraded right now. Read-only, except for one opt-in tool that signs a compromised account out everywhere.
Why this instead of the official Microsoft MCP Server for Enterprise
Microsoft ships an official MCP Server for Enterprise for Entra ID data. It is a good fit for an interactive admin at a keyboard, and is not a fit for an unattended triage bot:
- Delegated auth only. The official server does not support app-only
(client credentials) auth, so it cannot run headless behind a service
account. entraadm-mcp is built for that case: app-only in production, with
a delegated (
az login) fallback for local development. - A general-purpose Graph query tool, not a fixed tool set. The official
server exposes one tool that lets the model construct arbitrary
GET/schema-discovery calls against Microsoft Graph. That is flexible for a human, and awkward to put behind an allow-list for an automated triage profile. entraadm-mcp exposes fourteen fixed, read-only tools instead (plus one opt-in write, below). - No AADSTS translation. Sign-in failures come back as raw error codes; triage still needs a lookup table. entraadm-mcp annotates every sign-in failure with what the code actually means.
- No cross-request aggregation. Microsoft Graph itself cannot filter
sign-ins on
status/errorCodeserver-side, and has no built-in password-spray view.signin_failure_statsaggregates client-side and flags IPs with failed sign-ins against many distinct users — the pattern Entra's per-account smart lockout does not catch on its own.
Tools
| Tool | What it answers |
|---|---|
health_check |
Is Graph reachable, and can this credential read sign-in logs? |
get_user |
Is this account enabled, synced from on-prem, and what are its licenses? |
signin_logs |
Why did this user's sign-in fail (or succeed), with the AADSTS code translated? |
signin_failure_stats |
Tenant-wide failure aggregation: top error codes, users, apps, source IPs, and password-spray suspects |
signin_success_stats |
Tenant-wide success aggregation by source IP: IPs shared by several accounts, legacy-auth (SMTP/IMAP) successes — the view that finds a breach |
signin_by_ip |
Every sign-in from one source IP: who got in from it, who was tried, and when |
directory_audits |
Who changed what in the directory (block/unblock, attribute edits), and when? |
get_user_auth_methods |
Is MFA actually registered for this account? |
list_subscriptions |
Which Azure subscriptions can this credential see? (ARM) |
vm_backup_status |
Which Azure VMs are backed up by Azure Backup, did the last backup succeed, and is any recovery point stale? (ARM) |
vm_backup_policies |
What schedule and retention do the Azure VM backup policies have? (ARM) |
vm_activity |
Did someone restart this Azure VM, or did Azure (host failure, maintenance)? Activity log and Resource Health for a time window (ARM) |
m365_service_health |
Is a Microsoft 365 service degraded, and which incidents and advisories are open? |
daily_brief |
One-call summary combining m365_service_health, signin_failure_stats and directory_audits |
Every tool above is read-only. One state-changing tool exists and is off by default:
| Tool | What it does |
|---|---|
revoke_sign_in_sessions |
Signs one account out everywhere (Graph revokeSignInSessions: refresh tokens and session cookies are invalidated). For a confirmed compromise. Registered only when ENTRAADM_ENABLE_REVOKE_SESSIONS=1 |
It looks the account up by exact userPrincipalName first and refuses a guest.
Every result has the same keys, including revoked and not_covered: it does
not disable the account, reset the password or remove MFA methods, and access
tokens already issued keep working until they expire (typically about an
hour). revoked is true when Graph confirmed it, false when nothing was
done (a refusal, a deadline before the request was sent, a 4xx from Graph), and
null when the outcome is unknown (a response lost to a network error, a 5xx
after the retries, a deadline after the request was sent, an unreadable success
answer); calling again is then safe, revocation is idempotent. Leave the
switch unset on any deployment that is not behind its own approval step (a
shared gateway, for instance) -- the tool is then absent from tools/list.
Unblocking an account and resetting a password stay out of scope.
The four Azure Resource Manager tools live in an Entra-named server on
purpose: they use the same app registration and the same azure-identity
credential object (a second token scope, https://management.azure.com/.default),
run in the same deployment and are covered by the same health_check. A
separate server for these tools would double the operational surface. They
are governance reads, not resource management; a subscription the credential
has no Reader role on simply does not appear in list_subscriptions.
Auth model
Two auth modes, selected by which environment variables are set:
| Mode | When | Env vars |
|---|---|---|
| app-only | All three set | ENTRAADM_TENANT_ID, ENTRAADM_CLIENT_ID, ENTRAADM_CLIENT_SECRET |
| azure-cli | None set | (uses the current az login session) |
Setting one or two of the three app-only variables is a configuration error and the server refuses to start, rather than silently falling back to a different auth mode than intended.
Required Graph permissions
| Tool(s) | Permission | Notes |
|---|---|---|
get_user (base fields) |
User.Read.All |
|
signin_logs, signin_failure_stats, signin_success_stats, signin_by_ip, directory_audits, get_user's sign_in_activity field |
AuditLog.Read.All (app-only) or the Reports Reader directory role (delegated) |
|
get_user_auth_methods |
UserAuthenticationMethod.Read.All |
App-only only; not available under delegated (az login) auth in a typical tenant role assignment |
m365_service_health, daily_brief's service_health section |
ServiceHealth.Read.All |
Under delegated auth the signed-in user also needs an Entra role that may read service health (for example Service Support Administrator or Global Reader) |
revoke_sign_in_sessions (opt-in) |
User.RevokeSessions.All (app-only) or the User Administrator / Helpdesk Administrator directory role (delegated; Privileged Authentication Administrator for an administrator account) |
Only when ENTRAADM_ENABLE_REVOKE_SESSIONS=1. A write permission: grant it only to a deployment that needs the tool. The account lookup before it needs User.Read.All. Without the permission the tool answers missing_permission for the auth mode in use and nothing changes |
Required Azure RBAC
| Tool(s) | Role | Notes |
|---|---|---|
list_subscriptions |
none | Lists whatever the credential can see; an empty list means no role has been granted anywhere yet |
vm_backup_status, vm_backup_policies, vm_activity |
Reader on each subscription | Reader is */read and covers every endpoint used. Backup Reader alone is not enough (it has no Microsoft.Compute/virtualMachines/read). Granted to the service principal (app-only) or to the signed-in user (azure-cli) |
Azure RBAC is optional: health_check reports ARM reachability and the number
of visible subscriptions as an informational arm probe that never changes
status, so a deployment used only for Entra triage stays healthy.
A missing permission never crashes a tool. It degrades that tool (or that
one field) to {"error": "...", "missing_permission": "..."} with a
human-readable explanation of what role or permission is needed, so
health_check and every other tool stay usable even before full permissions
are granted.
Setup
uv tool install entraadm-mcp
# or
pip install entraadm-mcp
Configuration
Set the three app-only variables for production/unattended use:
export ENTRAADM_TENANT_ID=00000000-0000-0000-0000-000000000000
export ENTRAADM_CLIENT_ID=00000000-0000-0000-0000-000000000000
export ENTRAADM_CLIENT_SECRET=your-client-secret
Or leave all three unset and run az login first for local development.
Optional:
# Default page cap for the log-scanning tools (1-50, default 50).
export ENTRAADM_MAX_PAGES_DEFAULT=50
# Wall-clock budget per tool call in seconds (default 45; 0 disables). A hosted MCP
# client cuts a call off at about 60 s, so a scan stops at the budget and returns
# what it has with capped=true.
export ENTRAADM_DEADLINE=45
# Your own address ranges (NAT egress, offices, labs). Spray suspects inside them are
# listed separately as internal_spray_suspects, labelled with the site.
export ENTRAADM_SITES_INI=/path/to/sites.ini
# Register revoke_sign_in_sessions, the one state-changing tool (exactly "1"; anything
# else leaves it off). Also needs the User.RevokeSessions.All permission.
export ENTRAADM_ENABLE_REVOKE_SESSIONS=1
ENTRAADM_SITES_INI uses the same format as keycloak-mcp's KEYCLOAK_SITES_INI,
so one file can serve both (see sites.ini.example). Lookups are
longest-prefix. A file that is set but cannot be read makes health_check report
degraded, and every IP is then treated as external. An invalid CIDR is reported the
same way, but the valid ranges are still used.
Usage
Claude Code (plugin)
/plugin marketplace add shigechika/entraadm-mcp
/plugin install entraadm-mcp@entraadm-mcp
Claude Code (manual)
Add to .mcp.json:
{
"mcpServers": {
"entraadm-mcp": {
"type": "stdio",
"command": "uvx",
"args": ["entraadm-mcp"],
"env": {
"ENTRAADM_TENANT_ID": "${ENTRAADM_TENANT_ID:-}",
"ENTRAADM_CLIENT_ID": "${ENTRAADM_CLIENT_ID:-}",
"ENTRAADM_CLIENT_SECRET": "${ENTRAADM_CLIENT_SECRET:-}"
}
}
}
}
Direct execution
entraadm-mcp
CLI options
| Option | Effect |
|---|---|
--version |
Print the version and exit |
--check |
Resolve auth, probe Graph reachability and sign-in log access, print a report (plus an informational ARM line), exit 0 (or 1 on config error) |
Notes
- Coverage contract. Every result that walks a paged Graph collection
carries a
cappedboolean when its window was not fully scanned — a partial scan is never reported as if it were exhaustive. found: falseis not an error.get_userandget_user_auth_methodsanswer a nonexistent account with{"found": false, ...}, not anerrorkey — a typo'd userPrincipalName should never look like this server being broken.- Retention. Entra ID P1 retains sign-in and directory audit logs for 30 days. A window beyond that returns an empty result, not an error.
Development
uv sync --dev
uv run pytest -v
uv run ruff check .
uv run ruff format --check .
Live smoke test
uv run python scripts/smoke_test.py
Read-only (revoke_sign_in_sessions is skipped by name even when enabled), no
payloads printed (tool names/statuses/row counts only), and bounded (small explicit windows/page caps) — nothing here writes to the
tenant or scans more than a day of logs.
Releasing
This repository uses release-please
driven by Conventional Commits. Merge
a feat:/fix: PR to main, and release-please opens (or updates) a
release PR; merging that PR tags a release and triggers the publish pipeline
(PyPI, MCP Registry).
License
MIT
Metadata
Release files for entraadm-mcp 1.5.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| entraadm_mcp-1.5.0.tar.gz | 87.6 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| entraadm_mcp-1.5.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 140.4 kB
Release files / entraadm_mcp-1.5.0.tar.gz
| Download URL | entraadm_mcp-1.5.0.tar.gz |
|---|---|
| Size | 87.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
9e8aa7cdae813322a78f77b4ceb037c3093e083b00dc5e9c795a28660f833841
|
|
BLAKE2b-256 checksum How to use checksums |
25e074180733d725350f57990a1735888bb81ed2f6dfcdce3c563742045b454d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 24, 2026.
Transparency logRelease files / entraadm_mcp-1.5.0-py3-none-any.whl
| Download URL | entraadm_mcp-1.5.0-py3-none-any.whl |
|---|---|
| Size | 52.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
cf04ac004b8b3be0a478b9be74dea4870048c0fd6c50df694470c6f656fc1a29
|
|
BLAKE2b-256 checksum How to use checksums |
c801c83905288664b877e93fb922f6c47a09e863f2f527d03ce5192336c0ef68
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 24, 2026.
Transparency logSecurity for entraadm-mcp 1.5.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for entraadm_mcp-1.5.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 24, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.