Skip to main content
Glama
entraadm-mcp

entraadm-mcp

by shigechika

Server Configuration

Describes the environment variables required to run the server.

NameRequiredDescriptionDefault
ENTRAADM_DEADLINENoWall-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.45
ENTRAADM_CLIENT_IDNoMicrosoft Entra ID application (client) ID, used for app-only (client credentials) authentication
ENTRAADM_TENANT_IDNoMicrosoft Entra ID tenant ID, used for app-only (client credentials) authentication
ENTRAADM_CLIENT_SECRETNoMicrosoft Entra ID client secret, used for app-only (client credentials) authentication
ENTRAADM_MAX_PAGES_DEFAULTNoDefault page cap for the log-scanning tools (1-50, default 50).50

Instructions

Guidance the server publishes about itself, which clients place ahead of the tool catalog so the model reads it before choosing anything.

This server publishes no instructions, or was last inspected before Glama recorded them.

Capabilities

Features and capabilities supported by this server

Protocol revision2025-11-25

CapabilityDetails
tools
{
  "listChanged": false
}
prompts
{
  "listChanged": false
}
resources
{
  "subscribe": false,
  "listChanged": false
}
experimental
{}

Tools

Functions exposed to the LLM to take actions

NameDescription
health_checkA

Fleet-standard health probe: service/version/status plus two independent Graph probes.

graph confirms Microsoft Graph is reachable at all (GET /users with $top=1 -- needs only User.Read.All, the minimum permission every deployment of this server needs anyway). signin_probe additionally confirms the current credential can read sign-in logs -- the permission every other tool here except get_user depends on. Both probes always run, independently of each other: a tenant that has AuditLog.Read.All but not (yet) the baseline User.Read.All would otherwise have this report "Graph unreachable" -- a fabricated diagnosis, since Graph plainly is reachable if the other probe succeeds. status is derived from the two outcomes: healthy when both succeed, degraded when exactly one does (Graph is reachable but some permission is missing), error only when neither does.

Read-only. Always returns the same keys regardless of outcome (detail is null on success, a translated message on failure), so a caller never has to branch on which keys are present.

sites reports the optional ENTRAADM_SITES_INI table (configured, ranges, error). A configured file that fails to load is an error here and leaves status no better than degraded: without it every internal IP would be reported as an external spray suspect.

With a tenant file (ENTRAADM_CONFIG) every tenant is probed. The top-level keys stay the DEFAULT tenant's, as in a single-tenant deployment, plus tenant (the default's name) and tenants: {name: {default, domains, auth_mode, status, graph, signin_probe}} for every tenant, the default included. Each tenant's probes run under an even share of the call budget (ENTRAADM_DEADLINE), so a tenant that does not answer is an error there instead of stalling the call. Another tenant that is not healthy makes the top-level status at most degraded (the default tenant alone can make it error). arm is the default tenant's only. Env-only results carry neither key.

identity_protection is the Identity Protection licence probe (licensed / not_licensed / no_permission / error, cached per tenant for an hour; see compromise_suspects), per tenant in tenants too. Informational: not_licensed (no Entra ID P2) and the rest never change status.

home_countries reports ENTRAADM_HOME_COUNTRIES (configured, countries, error), which compromise_suspects needs for its foreign-success finding. Informational: unset or invalid never changes status.

arm is a third, informational probe (Azure Resource Manager reachable, and how many subscriptions this credential can see). It never moves status: Azure RBAC is optional for this server, and a deployment used only for Entra triage must not sit at degraded forever because nobody granted it a Reader role. It runs last under its own short budget (_ARM_PROBE_SECONDS) so an unresponsive ARM cannot push this whole call past the client's tool-call timeout. The budget bounds the request, not the first token acquisition (azure-identity is synchronous; in azure-cli mode it shells out to az), which is the same limit every tool here has.

get_userA

One account's identity/lifecycle state -- the first thing to check on any triage report.

account_enabled=false means the account itself is the whole story; stop there. A stale last_password_change_date_time alongside a fresh "wrong password" complaint (AADSTS50126 in signin_logs) is the most common on-the-ground pattern: the password changed or expired somewhere, and a cached credential on one device is now stale. on_premises_sync_enabled=true means this account is synced from an on-premises directory (Entra Connect) -- Entra is a downstream copy of its password via Password Hash Sync, not the source of truth. licenses names are resolved from the tenant's SKU catalog (/subscribedSkus, page budget from ENTRAADM_MAX_PAGES_DEFAULT); licenses_capped: true appears only when that scan was cut short before resolving one of this account's own licenses -- when present, one or more licenses entries is a raw skuId rather than a friendly name.

sign_in_activity needs an additional Graph read (AuditLog.Read.All application permission, or -- for azure-cli auth -- the Reports Reader directory role) beyond what the rest of this tool needs. If that permission is missing, every other field above still returns and sign_in_activity alone degrades to {"error": ..., "missing_permission": "AuditLog.Read.All"}.

A nonexistent account is a normal answer, not a tool failure: the result is {"found": false, "user_principal_name": upn} rather than an error key, so a typo'd UPN in a triage report cannot be mistaken for this tool being broken.

Read-only (User.Read.All application permission, or an equivalent delegated read). Requires an exact userPrincipalName, not a display name or partial match.

Args: upn: The account's userPrincipalName, e.g. "user@example.edu". tenant: Search this tenant instead of the one claiming the UPN's domain (section name, domain or tenant id): for a .onmicrosoft.com or #EXT# UPN, or a B2B guest's sign-ins in the host tenant.

signin_logsA

One user's recent sign-in events, AADSTS-annotated.

The most direct answer to "why can't this person log in": each entry's error_code_meaning translates the raw AADSTS code (e.g. 50126 -> "invalid credentials (wrong password)") so triage rarely needs a second lookup. result filters client-side after the Graph fetch (Graph cannot filter sign-ins on status/errorCode server-side): "failure" (the default) keeps only failed attempts, "success" keeps only clean ones, "all" keeps everything.

Because the filter is client-side, this walks pages until it has collected top matching entries or exhausts max_pages -- a mostly- successful user can otherwise mean paging through hundreds of rows to find a handful of failures. capped=true means the page budget ran out (or top was reached) before the whole window was scanned; a low match count alongside capped=true is evidence of "no more found within the budget", not "no more exist".

Read-only (AuditLog.Read.All application permission, or -- for azure-cli auth -- the Reports Reader directory role). Entra ID P1 retains sign-in logs for 30 days; hours beyond that returns an empty result, not an error.

Args: user: The account's userPrincipalName. hours: How far back to look, clamped to [1, 720] (30 days). result: "failure" (default), "success", or "all". top: Maximum matching entries to return, clamped to [1, 500]. max_pages: Page budget for the client-side filter walk (default: ENTRAADM_MAX_PAGES_DEFAULT). tenant: Search this tenant instead of the one claiming the UPN's domain (section name, domain or tenant id): for a .onmicrosoft.com or #EXT# UPN, or a B2B guest's sign-ins in the host tenant.

signin_by_ipA

Every sign-in from one source IP: who got in from it, who was tried, and when.

The follow-up to a spray_suspects or shared_ips hit: Graph can filter sign-ins on ipAddress server-side, so this is one cheap query, not a log walk. users summarises the IP per account (successes, failures, first/last seen, up to 50 accounts) over every row fetched; events lists the newest top entries that match result ("all" by default, or "success" / "failure"), each carrying the account name and the same AADSTS annotation as signin_logs. capped=true means the page budget or the deadline ran out before the window was fully read; events_truncated=true means more matching rows were read than top returns (the users summary still counts them).

Read-only (AuditLog.Read.All application permission, or -- for azure-cli auth -- the Reports Reader directory role).

Args: ip: The source IPv4 or IPv6 address, exactly as the sign-in log shows it. hours: How far back to look, clamped to [1, 720] (30 days). result: "all" (default), "success", or "failure" -- which events to list. top: Maximum events to return, clamped to [1, 500]. max_pages: Page budget (default: ENTRAADM_MAX_PAGES_DEFAULT). tenant: Tenant to read: a [tenant.] section name or one of its domains (default: the default tenant).

signin_failure_statsA

Tenant-wide sign-in failure aggregation -- the Entra ID counterpart to the RADIUS failure patrol.

Time-bounded (ENTRAADM_DEADLINE, default 45 s): on a wide window or a busy day the scan stops early and capped=true marks the counts as a lower bound; narrow hours for a full count.

Aggregates failed sign-ins across the whole tenant into four views: top AADSTS error codes (with the same meaning annotations as signin_logs), top failing users, top applications, and top source IPs. spray_suspects flags any IP with failed sign-ins against 5 or more distinct users -- Entra's smart lockout is per-account, so a low-and-slow password spray from one IP across many accounts does not trip it the way a brute force against one account does; this is the observation a per-account view cannot make on its own. This mirrors the KeyCloak-side spray detection this fleet already relies on; neither the official Microsoft MCP Server for Enterprise nor Graph itself offers this aggregation.

When ENTRAADM_SITES_INI names the tenant's own address ranges, IPs inside them get site (and site_name) in top_ips, and suspects inside them move from spray_suspects to internal_spray_suspects: a NAT egress or a room of shared workstations fails for many users every day, and would otherwise bury an outside source. Internal suspects are kept, not dropped, because an internal host can be compromised too. sites reports whether the file loaded; an invalid CIDR is reported in sites.error while the valid ranges are still used, and a file that cannot be read leaves every IP in spray_suspects.

Read-only (AuditLog.Read.All application permission, or -- for azure-cli auth -- the Reports Reader directory role). Graph cannot filter sign-ins on status/errorCode server-side, so this walks up to max_pages of the full sign-in log for the window and aggregates client-side -- capped=true means the page budget ran out before the window was fully scanned, so the counts below are a sample of the window, not a census of it.

Args: hours: How far back to look, clamped to [1, 720] (30 days). max_pages: Page budget (default: ENTRAADM_MAX_PAGES_DEFAULT). tenant: Tenant to read: a [tenant.] section name or one of its domains (default: the default tenant).

signin_success_statsA

Tenant-wide successful sign-in aggregation by source IP -- the view that finds a breach.

signin_failure_stats shows who is being attacked; this shows whether anyone got in. The breach signature is one source IP signing in successfully as several different accounts, most often over a legacy protocol (clientAppUsed such as "Authenticated SMTP" or "IMAP4", which carry no MFA). shared_ips lists the IPs with successes for min_distinct_users or more distinct accounts, most-shared first (up to 50 IPs, shared_ips_capped when more qualified; account names up to 25 per IP), with the client apps and the countries seen. legacy_auth_users lists the accounts that succeeded over a legacy protocol at all, with how many IPs and countries they came from.

A campus NAT, a VDI farm or a shared proxy also puts many accounts behind one IP, so a shared IP is a lead, not a verdict: the caller excludes its own egress ranges and reads the client apps and countries before calling anything a breach. Graph cannot filter sign-ins on status/errorCode server-side, so like signin_failure_stats this walks the sign-in log for the window and aggregates client-side. The walk covers interactive sign-ins only (Graph's default listing): every legacy-protocol authentication is logged as interactive, so none is missed, but non-interactive token refreshes are not counted; capped=true means the page budget or the deadline (ENTRAADM_DEADLINE, default 45 s) ran out first and the counts are a lower bound -- narrow hours for a full count.

Read-only (AuditLog.Read.All application permission, or -- for azure-cli auth -- the Reports Reader directory role).

Args: hours: How far back to look, clamped to [1, 720] (30 days). max_pages: Page budget (default: ENTRAADM_MAX_PAGES_DEFAULT). min_distinct_users: Distinct accounts an IP needs to appear in shared_ips (default 2, clamped to >= 2). tenant: Tenant to read: a [tenant.] section name or one of its domains (default: the default tenant).

compromise_suspectsA

Accounts that were probably taken over, from one walk of the sign-in log.

The success-side counterpart of signin_failure_stats' spray suspects. Two findings, both from EXTERNAL source IPs (outside the address ranges in ENTRAADM_SITES_INI; without that file every IP counts as external):

  • spray_breaches (severity high): an IP with credential failures (wrong password, unknown, locked or disabled account -- not MFA prompts, session interrupts or Conditional Access blocks, which follow a correct password) for min_spray_users or more distinct accounts in the window AND a successful sign-in as at least one account. Without internal ranges (ENTRAADM_SITES_INI unset or unreadable, internal_ranges_known: false) these are medium with a note: the tenant's own NAT egress can look like a spray source. One row per account and IP: failed_distinct_users from that IP, first/last success, client apps, countries, legacy_auth. The strongest signal: a spray that found a working password.

  • foreign_successes: successes from a country outside ENTRAADM_HOME_COUNTRIES (assigned ISO 3166-1 alpha-2 codes, e.g. "JP"; one invalid code disables this list, see home_countries.error), grouped per account with countries, the most used IPs (10, with counts), client apps, first/last, and home_success_in_window (the account also signed in from a home country or an internal IP in the window; null when not seen in a capped scan, since the unread part may hold it). high when a legacy protocol was used (password only, no MFA), the account is also a high spray_breaches entry, the success came through Azure CLI / Azure PowerShell / Azure AD PowerShell (admin_tools; how stolen passwords are tested and used), or the successes span 3 or more countries within 24 hours (a rotating proxy pool, not a traveller); high_reasons names which. medium otherwise. Unset home countries: home_countries.configured is false and this list stays empty -- nothing is guessed. A success with no country is never called foreign.

  • microsoft_risky_users: Microsoft's own verdicts from Identity Protection, which needs Entra ID P2 and the optional IdentityRiskyUser.Read.All permission. A cheap probe (cached per tenant) decides: without P2 the section is {"available": false, "reason": "not_licensed" | "hidden" | "no_permission" | "error"} and nothing else changes -- never read it as "no risky users". With P2: open (atRisk / confirmedCompromised) high and, separately, medium risky users (50 each, *_capped), each marked when also in spray_breaches / foreign_successes.

counts are distinct accounts over every finding (case-insensitive, counted before the lists are cut): high is any account high in this tool's own lists or in Microsoft's high list; medium is any account medium here or in Microsoft's medium list that is not high. With P2, microsoft_high / microsoft_medium say how many accounts Microsoft lists. Lists are capped at 50 (*_capped). how_to_read says what to act on. Like signin_failure_stats this walks the interactive sign-in log for the window (every legacy-protocol authentication is interactive) under ENTRAADM_DEADLINE; capped=true means the window was not fully read and the findings are a lower bound.

Read-only (AuditLog.Read.All application permission, or -- for azure-cli auth -- the Reports Reader directory role; IdentityRiskyUser.Read.All, optional, for microsoft_risky_users).

Args: hours: How far back to look, clamped to [1, 720] (30 days). tenant: Tenant to read: a [tenant.] section name or one of its domains (default: the default tenant). min_spray_users: Distinct failed accounts that make an IP a spray source (default 5, clamped to >= 2). max_pages: Page budget (default: ENTRAADM_MAX_PAGES_DEFAULT).

directory_auditsA

Who did what to the directory, and when -- the operator-side counterpart to signin_logs.

Every admin action against a user object (block/unblock, password reset, role assignment, attribute edits) appears here, naming the actor (initiated_by) and the affected object(s) (target_resources). This is the record a manual "unblock and reset" intervention -- like the one that closed the 2026-08-21 case this server exists to shorten -- leaves behind; it is how a later triage can tell "already handled by a human" from "still open".

user, when given, matches audits where that account is either the initiator or a target resource. Graph's directoryAudits endpoint only supports server-side $filter on the initiator (initiatedBy/user/userPrincipalName), not on targetResources, so this fetches the full time window and matches both sides client-side -- a window with many unrelated admin actions can need a larger max_pages budget than signin_logs/signin_failure_stats to find one specific user's audits; capped=true warns when that budget ran out before the window was fully scanned.

Read-only (AuditLog.Read.All application permission, or -- for azure-cli auth -- the Reports Reader directory role). Entra ID retains directory audit logs for 30 days, same as sign-in logs.

Time-bounded: the scan stops after ENTRAADM_DEADLINE seconds (default 45) and returns what it has with capped=true. Over several days the log is dominated by device-registration noise ("Update device"), so pass category (for example UserManagement, RoleManagement, GroupManagement, ApplicationManagement) to have Graph filter server-side; that keeps a multi-day window inside the budget.

Args: category: Only this Graph audit category (letters only; default: all). user: Restrict to audits naming this userPrincipalName as actor or target (default: all). hours: How far back to look, clamped to [1, 720] (30 days). top: Maximum records to return, clamped to [1, 500]. max_pages: Page budget (default: ENTRAADM_MAX_PAGES_DEFAULT). tenant: Tenant to read: a [tenant.] section name or one of its domains (default: the default tenant).

get_user_auth_methodsA

Registered authentication methods for one account -- is MFA actually set up?

mfa_registered answers "would this account survive a password-spray hit": True iff at least one non-password method is registered (Authenticator app, phone, FIDO2 security key, Windows Hello, a temporary access pass, software OATH token, or a platform credential/passkey). password itself is excluded from that count -- every account has one, so its presence alone says nothing about MFA coverage.

A nonexistent account is a normal answer, not a tool failure: the result is {"found": false, "user_principal_name": upn} rather than an error key, matching get_user's contract.

Read-only (UserAuthenticationMethod.Read.All application permission). This endpoint is app-only only: it is not exposed to delegated (azure-cli) auth under this tenant's current role assignment, so it degrades to a permission error under azure-cli auth even when other tools work.

Args: upn: The account's userPrincipalName. tenant: Search this tenant instead of the one claiming the UPN's domain (section name, domain or tenant id): for a .onmicrosoft.com or #EXT# UPN, or a B2B guest's sign-ins in the host tenant.

list_subscriptionsA

The Azure subscriptions this credential can see, as ARM lists them.

Azure Resource Manager in the DEFAULT tenant only, also when several tenants are configured (ENTRAADM_CONFIG).

Read-only (GET /subscriptions; needs no RBAC beyond being able to see a subscription at all). The discovery anchor for vm_backup_status and vm_backup_policies: both accept a subscription id or display name from this list. An empty list with auth working means no Reader role has been granted anywhere yet. capped marks a listing cut short by the page budget.

vm_backup_statusA

Which Azure VMs are backed up by Azure Backup, and whether the last backup succeeded.

Azure Resource Manager in the DEFAULT tenant only, also when several tenants are configured (ENTRAADM_CONFIG).

Joins every VM of a subscription (Microsoft.Compute/virtualMachines) with the Azure-VM protected items of its Recovery Services vaults, on the VM's ARM id (case-insensitively; ARM casing differs between the two services). Read-only; needs the Reader role on the subscription.

Per VM:

  • protected: true when a protected item exists with protectionState Protected or IRPending (first backup not done yet, note says so); false when there is none, or it is ProtectionStopped / ProtectionPaused / ProtectionError (data may be retained, backups are not running -- the state is shown); null (unknown) when no item was found but the vault scan was incomplete (capped, a skipped vault, an unreadable or unreached subscription -- in the global join, any subscription's) -- a partial scan never produces a false "unprotected".

  • backup_ok: true for lastBackupStatus Completed / CompletedWithWarnings, false for Failed, null before any backup ran or while one is running (InProgress, Warning, an unknown value). An item whose protectionState is missing is protected: null with a note. When two vaults hold an item for one VM, the one still backing up wins (duplicate_items counts them).

  • stale: no recovery point (lastRecoveryPoint, the proof of a successful backup) within stale_hours. The default 36 h suits daily policies; raise it for weekly ones (policy_name is in the row).

subscription is an id or display name from list_subscriptions; None scans every visible subscription and joins globally, so a VM backed up to a vault in another subscription is matched. With one subscription named, only its vaults are scanned (coverage says so). An unknown or ambiguous name returns found: false.

orphans are protected items whose VM no longer exists (backup data retained for a deleted VM), reported only when that VM's subscription was listed completely. One subscription's permission error is reported on its own entry; the others still return. When the wall-clock budget runs out, the remaining subscriptions are listed under skipped and capped is set.

vm_backup_policiesA

Schedule and retention of the Azure VM backup policies in each Recovery Services vault.

Azure Resource Manager in the DEFAULT tenant only, also when several tenants are configured (ENTRAADM_CONFIG).

Read-only; needs the Reader role on the subscription. Per policy: policy_type (V1 = SimpleSchedulePolicy, V2 = SimpleSchedulePolicyV2 with an hourly / daily / weekly schedule), schedule (frequency, run times in UTC, weekdays, or the hourly window), retention (LongTermRetentionPolicy: daily / weekly / monthly / yearly blocks with their day selection and duration), instant_rp_days, timezone and protected_item_count (ARM's own count). A schedule or retention type this server does not know is returned as {"type": <name>, "unparsed": true}, never dropped.

subscription is an id or display name from list_subscriptions (None = every visible subscription); an unknown or ambiguous name returns found: false. Coverage flags as in vm_backup_status: skipped_vaults per subscription, skipped subscriptions when the budget ran out, capped overall.

vm_activityA

What happened to an Azure VM in a time window: operations people ran, and events Azure raised.

Azure Resource Manager in the DEFAULT tenant only, also when several tenants are configured (ENTRAADM_CONFIG).

Answers "did someone restart this VM, or did Azure?" from the Azure activity log of the VM's subscription. Read-only; needs the Reader role on the subscription. Two reads: the VM's resource group (server-side filter, so a busy subscription cannot crowd the VM's own events out of the page budget) feeds health and vm_operations; the whole subscription feeds the rest.

  • health: Resource Health events of the VM (cause PlatformInitiated = Azure, e.g. Redeploying due to host failure; UserInitiated = an operation on the VM, or Reboot initiated from inside the machine for a restart from the guest OS).

  • vm_operations: Administrative events on the VM and its sub-resources (restart, deallocate, redeploy, extension changes), with caller (a user principal name or an application id). One operation usually logs several events (Started, Accepted, Succeeded) sharing a correlation_id.

  • other_operations: Administrative events on any other resource of the subscription (count of events, operations = distinct correlation ids, items = the newest 50). Zero here and in vm_operations means nobody operated the subscription through Azure Resource Manager in the window; it says nothing about actions inside the guest OS.

  • service_health: Azure incidents and planned maintenance announced to the subscription.

  • other_categories: counts of the remaining categories (Alert, Policy, other resources' health events, ...).

vm is the VM name, matched case-insensitively across the subscriptions this credential can see; narrow it with subscription (id or display name from list_subscriptions) or resource_group when the name is not unique (ambiguous lists the candidates). A single match found while some subscription could not be listed completely is not taken as unique: it comes back as candidates with search_complete: false, to be confirmed by naming its subscription. Naming the subscription also keeps the search short: without it every visible subscription is listed, and the search may use at most half of the call's time budget. The window is start..end (ISO timestamps with an offset, e.g. 2026-10-21T21:00:00+09:00; seconds may be omitted), or the hours before end (default 24; end defaults to now); at most 168 hours, within the 90 days the activity log keeps. Times in the result are UTC as Azure returns them. capped marks a read cut short by the page or time budget (capped_reads says which): then an empty section is not proof that nothing happened.

m365_service_healthA

Microsoft 365 service status per service, and the open incidents and advisories.

Two Graph reads (ServiceHealth.Read.All): healthOverviews (one row per service with its current status) and the unresolved serviceAnnouncement/issues, newest first. A sign-in failure spike reads very differently while Entra or Exchange has an open incident, which is why daily_brief carries this section too. Each section degrades on its own ({"error", "missing_permission"}); summary is computed from whatever came back. capped means either collection was cut short by the page budget, so the counts are lower bounds. The message center (messages) needs a different permission and is not read.

Args: max_pages: Page budget for each collection (default: ENTRAADM_MAX_PAGES_DEFAULT). tenant: Tenant to read: a [tenant.] section name or one of its domains (default: the default tenant).

audit_log_search_startA

Start a Microsoft Purview audit log search: what was done in OneDrive/SharePoint, Exchange, Entra.

The follow-up to a confirmed takeover: the sign-in log shows which resources a session obtained tokens for, this shows what it then did (files opened or downloaded, mailbox rules and cmdlets, directory changes). A search runs asynchronously in Microsoft 365 and usually takes several minutes, so this only submits it (Graph POST /security/auditLog/queries) and returns query_id; call audit_log_search_result with that id (and the same tenant) until done is true.

The call creates a saved audit search object in the tenant (listed in the Purview audit search page); it changes no directory, mailbox or file data. Each call counts against the tenant's audit search limits (searches per day, searches running at once): a 429 comes back as rate_limited: true and is not retried. At least one filter besides the window is required, so a call never searches the whole tenant's log. The filters combine with AND.

Needs the application permission AuditLogsQuery.Read.All, or the service-scoped AuditLogsQuery-OneDrive.Read.All / -SharePoint.Read.All / -Exchange.Read.All / -Entra.Read.All (then set service to match). A 403 returns missing_permission. created is true, false (nothing was created), or null (the answer was lost; a search may exist anyway).

Args: user: Only records of this userPrincipalName. hours: How far back from end (or now) to search, clamped to [1, 4320] (180 days). Ignored with start. start: Window start, ISO 8601 with an offset (e.g. 2026-10-21T22:00:00+09:00). end: Window end, ISO 8601 with an offset (default: now). record_types: auditLogRecordType values, e.g. ["sharePointFileOperation", "exchangeItem"]. operations: Operation names, e.g. ["FileDownloaded", "New-InboxRule", "MailItemsAccessed"]. ip_addresses: Client IPv4/IPv6 addresses. keyword: Free text matched against the workload-specific content of each record (auditData), not against indexed properties such as the operation name; use operations for those (at most 256 characters). service: One workload, e.g. "Exchange", "SharePoint", "OneDrive", "AzureActiveDirectory". object_ids: Object ids, e.g. a file URL or a mailbox item id. tenant: Tenant to search: a [tenant.] section name, one of its domains or its tenant id (default: the tenant claiming the user's domain, else the default tenant).

audit_log_search_resultA

Status and, once finished, the summarised records of an audit search started by audit_log_search_start.

Reads Graph GET /security/auditLog/queries/{id}. While status is notStarted or running the answer is the status only (done: false); call again in a minute or two. failed, cancelled and any status this server does not know are done: true with an error. Once succeeded, the records are paged (/records) and come back as:

  • summary: total_records, first_seen/last_seen, counts by operation, service, record type and user, and per client IP the count with first/last seen (top 25 of each list; distinct has the full counts);

  • records: the newest top records, each with time, operation, service, record type, user, client IP, object id and a few details from auditData (client IP, user agent, result status, site URL, file name, the names of cmdlet parameters -- never their values), every string cut to 200 characters. The full auditData is not returned.

capped=true means the counts are lower bounds: either the page budget or the deadline ran out before every record was read (call again with a larger max_pages), or query.record_count_limit_exceeded is true -- Graph itself stopped collecting at its per-search record limit (query.record_count_limit), so the search holds only part of the matching records whatever max_pages is; narrow the window or the filters and start a new search. records_truncated means more records were read than top returns (the summary counts them all). An unknown id is found: false: a search exists only in the tenant that created it, so pass the tenant its start call used. Permissions as audit_log_search_start; a 403 returns missing_permission.

Args: query_id: The query_id returned by audit_log_search_start (a GUID). top: Maximum records to return, clamped to [1, 500]. max_pages: Page budget for the records (default: ENTRAADM_MAX_PAGES_DEFAULT). tenant: Tenant the search was started in: a [tenant.] section name, one of its domains or its tenant id (default: the default tenant).

daily_briefA

One-call morning-patrol summary: M365 service health, sign-in failures, spray suspects, admin actions.

Combines m365_service_health, signin_failure_stats, directory_audits and compromise_suspects (its counts and the top 5 of each list; summary.compromise_suspects has the high/medium counts) into one result with a compact summary on top. The failure stats and the compromise suspects come from one walk of the sign-in log. The shape matches this fleet's other daily_brief tools. A permission failure in one section degrades only that section's contribution to summary -- the other sections still return in full. Azure VM backup is deliberately not a section here: its ARM fan-out is the one scan that could exhaust the shared budget, so call vm_backup_status separately.

With a tenant file (ENTRAADM_CONFIG) it covers every tenant: the top-level keys stay the DEFAULT tenant's brief, exactly as in a single-tenant deployment, plus tenant (the default's name) and tenants ({name: the same brief} for each other tenant). The call budget (ENTRAADM_DEADLINE) is split: default first, each tenant gets an even share of what is left, so a busy tenant cannot starve the next one (a tenant that finishes early leaves its rest to the others). With ENTRAADM_DEADLINE=0 there is no budget and every tenant is scanned in full, one after another, which can outlast a client's ~60 s cutoff. Env-only results carry neither key.

Runs both sections synchronously in one tool call, unlike the sibling gwsadm-mcp's job+poll daily_brief. If this proves too slow for a tenant's sign-in volume against the client's tool-call timeout, port that job+poll pattern here (tracked in this repo's CLAUDE.md Roadmap).

Args: hours: How far back to look, clamped to [1, 720] (30 days). max_pages: Page budget passed to both sections (default: ENTRAADM_MAX_PAGES_DEFAULT). samples: Reserved for a future drill-down sample size; currently unused.

Prompts

Interactive templates invoked by user choice

NameDescription

No prompts

Resources

Contextual data attached and managed by the client

NameDescription

No resources

TDQS

A4.2/5.0

Scored across 17 tools

Disambiguation4/5

Most tools have clearly distinct scopes: per-user (get_user, signin_logs, get_user_auth_methods), tenant-wide aggregation (signin_failure_stats, signin_success_stats), and Azure VM tools (vm_backup_status, vm_backup_policies, vm_activity, list_subscriptions). The soft spots are the derived-signal tools — compromise_suspects overlaps conceptually with signin_failure_stats' spray_suspects and signin_success_stats' shared_ips, and daily_brief re-packages three other tools — but descriptions explicitly cross-reference each other, so an agent can tell them apart.

Naming Consistency4/5

All names are snake_case and domain-readable, with a coherent resource-oriented vocabulary (signin_*, vm_*, audit_log_*). Minor deviation: some tools use a verb prefix (get_user, list_subscriptions), others are bare nouns (signin_logs, directory_audits, daily_brief), and audit_log_search_start/_result use a suffix-style start/poll pair rather than a verb, so the convention is not uniform.

Tool Count4/5

17 tools for a multi-domain server (Entra sign-in triage, directory audit, user posture, Azure VM backup/activity, M365 health) is reasonable and each surface pulls its weight. It is slightly at the heavy end, and daily_brief is a convenience wrapper over three existing tools, so a small amount of consolidation is possible without losing capability.

Completeness4/5

Coverage is broad for a read-only triage server: identity/lifecycle, auth-method posture, sign-in success and failure analysis, compromise detection, directory audits, asynchronous Purview audit search with polling, service health, and Azure VM backup/activity with a subscription-discovery anchor. The notable gap is response actions — no tool to revoke sessions, disable an account, or force a password reset — which the descriptions acknowledge as manual interventions, leaving the 'contain' half of the workflow outside the tool surface.

Maintenance

ActivityActive
ResponsivenessNo recent community issues