Odel
entraadm MCP

entraadm MCP

Local
@shigechikaPythonMITUpdated 2w ago

MCP server for Microsoft Entra ID sign-in and audit-log triage (read-only)

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/errorCode server-side, and has no built-in password-spray view. signin_failure_stats aggregates 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

ToolWhat it answers
health_checkIs Graph reachable, and can this credential read sign-in logs?
get_userIs this account enabled, synced from on-prem, and what are its licenses?
signin_logsWhy did this user's sign-in fail (or succeed), with the AADSTS code translated?
signin_failure_statsTenant-wide failure aggregation: top error codes, users, apps, source IPs, and password-spray suspects
directory_auditsWho changed what in the directory (block/unblock, attribute edits), and when?
get_user_auth_methodsIs MFA actually registered for this account?
daily_briefOne-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:

ModeWhenEnv vars
app-onlyAll three setENTRAADM_TENANT_ID, ENTRAADM_CLIENT_ID, ENTRAADM_CLIENT_SECRET
azure-cliNone 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)PermissionNotes
get_user (base fields)User.Read.All
signin_logs, signin_failure_stats, directory_audits, get_user's sign_in_activity fieldAuditLog.Read.All (app-only) or the Reports Reader directory role (delegated)
get_user_auth_methodsUserAuthenticationMethod.Read.AllApp-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

OptionEffect
--versionPrint the version and exit
--checkResolve 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 capped boolean when its window was not fully scanned — a partial scan is never reported as if it were exhaustive.
  • found: false is not an error. get_user and get_user_auth_methods answer a nonexistent account with {"found": false, ...}, not an error key — 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