entraadm-mcp¶
MCP server for Microsoft Entra ID sign-in and audit-log triage. Read-only.
Built for triaging "why can't this person sign in" reports without a human manually pulling sign-in logs, translating AADSTS codes, and checking the directory audit trail by hand — three separate steps that closed a real 2026-08-21 support case at this server's origin.
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 not a fit for an unattended triage bot:
- Delegated auth only — no app-only (client credentials) support, so it cannot run headless behind a service account. entraadm-mcp is built for that case.
- A general-purpose Graph query tool, not a fixed tool set — the official server exposes one tool that lets the model construct arbitrary Graph calls, which is awkward to put behind an allow-list for an automated triage profile.
- No AADSTS translation and no cross-request aggregation — sign-in
failures come back as raw codes, and Microsoft Graph itself cannot filter
sign-ins on
status/errorCodeserver-side or flag password-spray patterns.
Tools¶
| Tool | Purpose |
|---|---|
health_check |
Is Graph reachable, and can this credential read sign-in logs? |
get_user |
Account lifecycle state: enabled, on-prem sync, password age, licenses, sign-in activity |
signin_logs |
One user's sign-in events, each AADSTS error code annotated with what it means |
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? |
daily_brief |
One-call summary combining signin_failure_stats and directory_audits |
All tools are read-only. There is no write/mutating tool in this server — unblocking an account, resetting a password, and revoking a session are all out of scope.
Design notes¶
Two auth modes, chosen by which env vars are set. All three of
ENTRAADM_TENANT_ID/_CLIENT_ID/_CLIENT_SECRET selects app-only
(ClientSecretCredential) — the production path for a headless deployment.
None of the three selects delegated auth via the current az login session
(AzureCliCredential) — convenient for local development, but not every
tool works under it (get_user_auth_methods needs an application
permission that has no delegated equivalent in a typical role assignment).
Setting one or two of the three is treated as a configuration error, not a
silent fallback.
A missing permission degrades, it never crashes. Every tool that needs
AuditLog.Read.All (or, under delegated auth, the Reports Reader directory
role) reports the gap as {"error": "...", "missing_permission": "..."}
with a human-readable explanation of what to grant — not a stack trace, and
not a generic "forbidden." health_check's signin_probe field reports
this same gap at the server level (status: "degraded") so a caller knows
before trying any of the other tools.
A nonexistent account is a normal answer, not an error. get_user and
get_user_auth_methods respond to a typo'd or nonexistent
userPrincipalName with {"found": false, ...}, distinct from the error
key used for an actual failure (bad input shape, unreachable Graph, missing
permission).
Sign-in log retention is 30 days (Entra ID P1). A window beyond that returns an empty result, not an error — there's nothing there to find, and that's a legitimate answer.
Next steps¶
- Reference — every tool's parameters, the auth model, and CLI usage