entraadm-mcp
English | 日本語
MCP server for Microsoft Entra ID sign-in and audit-log triage. Read-only.
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 seven fixed, read-only tools instead. - 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 |
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? |
daily_brief | One-call summary combining signin_failure_stats and directory_audits |
Every tool is read-only. Write operations (unblocking an account, resetting a password, revoking a session) are out of scope for this server.
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, 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 |
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 5).
export ENTRAADM_MAX_PAGES_DEFAULT=5
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, 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, 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