Skip to main content
Glama
taitson

entra-mcp

by taitson

entra-mcp

CI Python 3.11+ Licence: MIT

An MCP server that gives an AI agent read access to Microsoft 365 — and refuses to run with more permission than its own tools require.

uvx --from git+https://github.com/taitson/entra-mcp entra-mcp serve

That command works right now, with no Azure tenant, no app registration and no credentials. It serves a fictional 25-person tenant so you can see what the tools do before deciding whether to point them at a real one.


The problem this exists for

Every guide to connecting an agent to Microsoft 365 tells you to register an app with Directory.ReadWrite.All and move on. That hands the agent — and anyone who reaches its credential — the ability to delete your tenant, so that it can list users.

Nobody wants that. It happens because the permission lives in the Azure portal, far away from the code, and nothing checks that the two agree. Working out the minimum set by hand means reading the docs for every endpoint you call, and then re-checking it every time a tool is added.

entra-mcp moves the permission next to the tool that needs it:

FIND_STALE_ACCOUNTS = declare(
    "entra_find_stale_accounts",
    {"User.Read.All", "AuditLog.Read.All"},
    because="Cross-references the user list with sign-in activity.",
)

From that, two things follow. entra-mcp scopes prints exactly what to grant, so the app registration is built from the code rather than guessed at. And on --live the server checks the other direction before serving anything: it reads the roles claim of its own access token — the permissions actually granted and consented — and refuses to start when the app holds more than the enabled tools need.

$ entra-mcp serve --live

  ✓ 4 tools enabled (read-only)
  ✓ scopes required: User.Read.All, Directory.Read.All,
      Organization.Read.All, AuditLog.Read.All

  ✗ REFUSED: the app holds 2 permissions beyond what these tools use
      · Directory.ReadWrite.All  (no enabled tool requires it)
      · Mail.Send                (no enabled tool requires it)

  Remove them from the app registration, or pass --allow-excess
  to start regardless (the permission stays granted either way).

A missing permission is reported separately and cannot be waived at all — over-permissioned is a governance problem, under-permissioned is a broken server.

A permission no enabled tool declares is not convenience. It is standing authority that nothing in the running process will ever exercise, and that an attacker who reaches the credential inherits in full.


Related MCP server: mcp-m365-mgmt

What it answers

The tools are chosen to compose, not to mirror the Graph API. The question they exist for is one every IT manager has and none can answer quickly:

"Which licences are we paying for that nobody has used in 90 days?"

Answering it by hand means the user list, the sign-in logs and the purchased SKUs, joined three ways. Which is why nobody does it, and why licences quietly accumulate on people who left the company.

> Which licences are we paying for that nobody has used in 90 days?

  6 accounts, 9 reclaimable seats:

  · 2 disabled accounts still holding licences
      Cameron White    287 days   Microsoft 365 E5
      Bianca Pisani    341 days   Microsoft 365 E5, Power BI Pro

  · 3 active but idle over 90 days
      Lee Gu           204 days   Microsoft 365 E5, Power BI Pro
      Alland Deyoung   156 days   Microsoft 365 E3
      Irvin Sayers     118 days   Microsoft 365 E5, Power BI Pro

  · 1 service account, never signed in interactively
      Svc-BackupAgent            Microsoft 365 E3

  By product:  E5 ×4   Power BI Pro ×3   E3 ×2

  Separately, 9 purchased seats are assigned to nobody at all.

Tools

Tool

Answers

Graph permission

entra_list_users

Who is in the tenant, filtered by department or account state

User.Read.All

entra_get_user

One person: profile, manager, groups, licences

User.Read.All, Directory.Read.All

entra_get_license_usage

Seats bought vs. seats assigned, per SKU

Organization.Read.All

entra_find_stale_accounts

Idle accounts and the licences they hold

User.Read.All, AuditLog.Read.All

All read-only. Write tools are planned for v1.0, disabled by default, opt-in per tool, with a dry-run mode.

Run entra-mcp scopes to print this list with the reason for each permission.


Demo mode

entra-mcp serve          # demo is the default
entra-mcp serve --demo

The fictional Contoso tenant has 25 accounts, five licence SKUs, two disabled-but-licensed accounts and one service account that has never signed in — enough shape for every tool to return something interesting.

Sign-in dates are stored as days ago and resolved against the clock at load. A fixture with absolute dates starts lying the week after it is written: every account drifts past the staleness threshold until the demo that showed six idle licences shows twenty-five.

Demo mode is not only for evaluation. It keeps the test suite offline, free and fast, which is what lets it run on every push.


Live mode

1. Register an app

Azure portal → Microsoft Entra IDApp registrationsNew registration.

Under API permissions, add these as Application permissions (not Delegated), then click Grant admin consent:

User.Read.All
Directory.Read.All
Organization.Read.All
AuditLog.Read.All

Under Certificates & secrets, create a client secret and copy the value immediately — it is shown once.

AuditLog.Read.All also requires Microsoft Entra ID P1 or higher on the tenant. Without it, signInActivity does not exist in Graph at all and entra_find_stale_accounts will tell you so rather than returning a misleading empty result.

2. Provide the credentials

cp .env.example .env   # then fill in the three values
entra-mcp serve --live

.env is gitignored. The server reads ENTRA_TENANT_ID, ENTRA_CLIENT_ID and ENTRA_CLIENT_SECRET from the environment, so a secret manager works just as well.

Every response is tagged "mode": "demo" or "mode": "live". An agent that cannot tell fictional data from a real tenant will report fictional licence counts as fact, and so will the person reading its answer.


The console

entra-mcp console

Opens on http://127.0.0.1:8765. No tenant, no credentials, no MCP client — the fastest way to see what the server does.

It is not an admin dashboard, and adding one would put this project in a crowded category it has no business competing in. It is the README's argument on a screen: every tool beside the Graph permission it declares, the union your app registration should hold, and — with --live — what the credential actually holds and where the two disagree.

  • Tools, each showing the permissions it costs you

  • Permissions, with the verdict: least privilege holds, over-permissioned, or missing a scope. In demo mode it says there is nothing to audit rather than showing a green tick it has not earned.

  • Run any tool, with a form built from its schema

  • The audit trail, live, so you can watch what a call records — and confirm it does not record the directory

entra-mcp console --live --port 9000

The same startup audit as serve: an over-permissioned credential refuses to open the console too.

The console has no authentication and binds to 127.0.0.1. It is a local tool for one operator on one machine. Binding it elsewhere with --host exposes an unauthenticated view of your directory, and the server says so when you try.


Audit trail

entra-mcp serve --live --audit-trail ./entra-mcp.jsonl

One JSON line per tool call:

{"at":"2026-08-30T16:12:04+00:00","tool":"entra_find_stale_accounts","mode":"live",
 "arguments":{"days":90,"licensed_only":true},"outcome":"ok","durationMs":412,
 "summary":{"accountsFound":6,"reclaimableSeats":9}}

"Who told the agent to disable that account?" is the question that stops agent adoption in any organisation with a compliance function. This is what makes the answer "here is the line".

What it deliberately does not record: the response body. A single entra_list_users reply carries names, job titles and licence assignments for the whole tenant; writing that on every call would turn an audit log into a shadow copy of the directory, sitting outside every access control that protects the real one. The line records what was asked and a summary of what came back — enough to reconstruct behaviour, not enough to be a breach on its own.

Argument values are truncated, and any argument whose name looks like a credential is masked. Off unless a path is given; ENTRA_AUDIT_TRAIL works too.


Use it with Claude Desktop

{
  "mcpServers": {
    "entra": {
      "command": "uvx",
      "args": ["--from", "git+https://github.com/taitson/entra-mcp", "entra-mcp", "serve", "--demo"]
    }
  }
}

Swap --demo for --live once the credentials are in place.


Development

git clone https://github.com/taitson/entra-mcp
cd entra-mcp
uv venv && uv pip install -e ".[dev]"
uv run pytest

99 tests, all of them without a tenant, credentials or network access.

On Windows, see Troubleshooting above before running pytest directly.

Layout:

src/entra_mcp/
  scopes.py    permission declarations, one per tool
  console.py   the local page — Starlette, no new dependency
  audit.py     reads the token's roles claim and compares it to the above
  graph.py     one client interface, two backends (fixture and live HTTP)
  tools.py     tool logic — plain functions, no MCP imports
  trail.py     the audit trail, and what it refuses to write
  server.py    MCP wiring: call, record, serialise

tools.py never imports the MCP SDK, and graph.py never imports tools.py. That is what keeps the logic testable without a transport and the backends swappable without touching the tools.


Troubleshooting

Windows: foi bloqueado pela política do Device Guard / "blocked by your organization"

Smart App Control, on by default in fresh Windows 11 installs, blocks freshly created unsigned executables — which is exactly what pip writes into Scripts\. The message mentions "your organization" even when no organisation is involved.

Call the module instead of the shim:

python -m pytest -q
python -m entra_mcp scopes
python -m entra_mcp serve --demo

python.exe is signed and allowed; the generated .exe wrappers are not. Do not turn Smart App Control off to work around this — it cannot be re-enabled without reinstalling Windows.

AuditLog.Read.All works but entra_find_stale_accounts returns nothing useful

signInActivity requires Microsoft Entra ID P1 or higher. Without it the field does not exist in Graph at all. The tool says so rather than returning an empty list that reads like "everyone is active".

The server starts but every tool returns a 403

Permissions were added to the app registration but admin consent was never granted. In the portal, the API permissions page shows a warning triangle until it is.


Roadmap

v0.1 — the four read tools, demo mode, tests. (done)

v0.2 — startup privilege audit; JSONL audit trail that summarises rather than copies; the local console. (you are here)

v0.3entra_list_devices (Intune compliance state), entra_list_group_members, entra_get_signin_activity.

v1.0 — write tier behind per-tool opt-in with dry-run; an evaluation harness that checks the agent picks the right tool and that the guardrails hold when it does not; Docker image; PyPI.


Licence

MIT.

Available Tools

4 tools
entra_find_stale_accountsA
Read-onlyIdempotent

Find accounts that have not signed in recently and the licences they hold.

Answers "which licences are we paying for that nobody uses?". Joins the user list, sign-in activity and the licence SKUs, and reports how many seats could be reclaimed, broken down by product. Disabled accounts that still hold a licence are listed first — they are pure waste.

Requires User.Read.All and AuditLog.Read.All. Sign-in activity also needs Microsoft Entra ID P1 or higher on the tenant; without it, Graph does not expose this data at all and the tool will say so.

ParametersJSON Schema
NameRequiredDescriptionDefault
daysNoMinimum days since the last successful sign-in for an account to count as stale.
licensed_onlyNoOnly accounts holding at least one licence. True answers 'what are we paying for'; false answers 'what should be cleaned up'.
include_never_signed_inNoInclude accounts with no recorded sign-in. Turn off to filter out service accounts, which never sign in interactively.

Output Schema

ParametersJSON Schema
NameRequiredDescription
resultYes

TDQS

A4.5/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Beyond the readOnly/idempotent annotations, the description discloses meaningful behavior: it joins user list, sign-in activity, and licence SKUs; lists disabled accounts first as pure waste; requires specific Graph permissions; and fails gracefully with an explanatory message when Entra ID P1 is missing. This is rich, non-obvious context.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Every sentence earns its place: purpose, business question, behavior, ordering, and prerequisites. The description is front-loaded with the core function and remains compact despite covering several important behaviors.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

The description is complete for a read-only reporting tool: it states what data is joined, what output is produced, what permissions are needed, and the tenant licensing prerequisite. An output schema exists, so return-value details are not required.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, and each parameter already has a clear meaning. The description does not add parameter-level detail beyond what the schema provides, so the baseline of 3 applies without additional credit.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description opens with a specific verb and resource: 'Find accounts that have not signed in recently and the licences they hold.' It then clarifies the real-world question it answers and explains the data join, making it clearly distinct from siblings like entra_list_users or entra_get_license_usage.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description explicitly frames when to use the tool: it answers 'which licences are we paying for that nobody uses?' and describes the reporting output. It does not explicitly name sibling alternatives or state when not to use it, but the use case is clear enough to route an agent correctly.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

entra_get_license_usageA
Read-onlyIdempotent

Show, per licence SKU, how many seats were purchased and how many are assigned.

unassigned seats are paid for and held by nobody — the cheapest saving in any tenant, since reclaiming them affects no user. For seats assigned to people who never use them, call entra_find_stale_accounts instead.

Requires Organization.Read.All.

ParametersJSON Schema
NameRequiredDescriptionDefault

No parameters

Output Schema

ParametersJSON Schema
NameRequiredDescription
resultYes

TDQS

A4.7/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnlyHint, idempotentHint, and destructiveHint=false, covering the safety profile. The description adds meaningful context beyond annotations by explaining the concept of unassigned seats and their savings value, as well as the required permission. This enriches the agent's understanding without contradicting the annotations.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is compact and well-structured: it leads with the core purpose, adds a practical insight about unassigned seats, routes the agent to a sibling tool for a related case, and closes with the permission requirement. Every sentence earns its place; there is no filler.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a zero-parameter read-only tool with an output schema present, the description covers the purpose, the usage context, the alternative tool, and the required permission. There is nothing meaningful missing for an agent to decide whether to invoke it correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The tool has zero parameters, so there is no parameter documentation burden. The schema is vacuously complete at 100% coverage, and the baseline for no-parameter tools is 4. No additional parameter explanation is needed.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description opens with a specific verb and resource: 'Show, per licence SKU, how many seats were purchased and how many are assigned.' It goes beyond a generic label and distinguishes itself from siblings by naming entra_find_stale_accounts as the tool for a different scenario, while the other siblings focus on users rather than license SKU seat counts.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It clearly states when to use this tool: to see purchased vs assigned seats per SKU, highlighting unassigned seats as the cheapest saving. It also provides an explicit exclusion: for seats assigned to people who never use them, call entra_find_stale_accounts instead. Additionally, it specifies the required permission, Organization.Read.All.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

entra_get_userA
Read-onlyIdempotent

Get one account's profile, manager, group memberships and licences.

Use after entra_list_users when a question is about a specific person.

Requires User.Read.All and Directory.Read.All.

ParametersJSON Schema
NameRequiredDescriptionDefault
userYesUser principal name (adele.vance@contoso.com) or object id.

Output Schema

ParametersJSON Schema
NameRequiredDescription
resultYes

TDQS

A4.5/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare read-only, idempotent, and non-destructive behavior. The description adds value by specifying required permissions (User.Read.All and Directory.Read.All) and by enumerating exactly what data is returned (profile, manager, group memberships, licences), which is not visible in the annotations.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Three sentences, each earning its place: the core action, the usage context, and the required permissions. It is front-loaded with the primary purpose and contains zero filler.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a one-parameter, read-only tool with an output schema present, the description provides everything an agent needs: what it does, when to use it, and what permissions are required. Nothing essential is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The schema provides 100% coverage for the single 'user' parameter with a clear description (UPN or object id). The description adds no additional parameter-level detail beyond reinforcing that this is for a specific person, which is already implied by the parameter semantics.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states a specific verb ('Get'), resource ('one account's profile, manager, group memberships and licences'), and scope that distinguishes it from sibling tools like entra_list_users, which lists multiple users. The phrase 'one account' immediately signals this is the single-user counterpart.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicitly names the preceding tool (entra_list_users) and gives the condition for when to use this tool: 'when a question is about a specific person.' This provides clear routing guidance and is the strongest form of usage guidance.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

entra_list_usersA
Read-onlyIdempotent

List accounts in the Microsoft 365 tenant with the licences each one holds.

Use this to answer "who works here", "who is in Finance", or as the first step before looking at a specific person. Results are paged: check has_more and raise offset to continue.

Requires the Graph application permission User.Read.All.

ParametersJSON Schema
NameRequiredDescriptionDefault
limitNoMaximum accounts to return.
offsetNoAccounts to skip, for paging through a large tenant.
departmentNoOnly accounts in this department, matched exactly (e.g. 'Finance', 'Engineering').
enabled_onlyNoExclude disabled accounts. Leave false when auditing licence waste — disabled accounts are the ones most likely to be holding one.

Output Schema

ParametersJSON Schema
NameRequiredDescription
resultYes

TDQS

A4.3/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnlyHint, idempotentHint, and non-destructive behavior. The description adds valuable behavioral context beyond this: pagination via `has_more` and `offset`, and the required Graph application permission User.Read.All. This is useful, actionable transparency, though it does not discuss rate limits or failure modes.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is tight and front-loaded: one sentence states the core purpose, a second gives practical use cases, and the final two sentences add essential operational details. Every sentence earns its place, and there is no redundant or filler content.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With a rich output schema, the description does not need to explain return values. It covers purpose, typical use cases, pagination behavior, and required permissions, and is paired with a fully documented parameter schema. An agent has enough information to select and invoke this tool correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, and the schema already documents limit, offset, department, and enabled_only thoroughly, including exact-match semantics and the audit-waste nuance for enabled_only. The description adds some context around paging, but does not materially expand parameter meaning beyond the schema, so the baseline of 3 is appropriate.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description states a specific verb and resource: "List accounts in the Microsoft 365 tenant with the licences each one holds." It is clearly distinct from the sibling tools, especially by emphasizing the per-user license information and by framing it as a first step before deeper user-specific investigation.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description gives concrete use cases: answering "who works here", "who is in Finance", and as a first step before looking at a specific person. It provides clear context but does not explicitly name alternatives or when-not-to-use conditions relative to the sibling tools, so it stops just short of a 5.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

TDQS

A4.5/5.0
Disambiguation5/5

Each tool targets a distinct resource or question: license SKU counts, full user list, a single user's details, and stale accounts. The one potentially overlapping pair (license usage vs. stale accounts) is explicitly disambiguated in the descriptions.

Naming Consistency5/5

All tools use a consistent entra_ prefix followed by a clear verb_noun pattern: get_license_usage, list_users, get_user, find_stale_accounts. Verb choice maps predictably to collection vs. single-item vs. filtered lookup.

Tool Count5/5

Four tools is a well-scoped set for a read-only Entra user and license audit server. Each tool covers a meaningful slice of the domain without redundancy or bloat.

Completeness4/5

The read-side is well covered: list users, get user details, summarize license usage, and find stale accounts. The main gap is the lack of any write or management operations, such as assigning/reclaiming licenses or disabling accounts, though the tool set appears intentionally focused on analysis rather than changes.

Maintenance

ActivityMaintained
ResponsivenessSyncing

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Connectors

Related MCP Servers

  • -
    license
    Not graded
    quality
    Not graded
    maintenance
    Provides secure access to Microsoft Entra ID (Azure AD) resources including users, devices, and applications through Microsoft Graph API. Enables querying organizational data with comprehensive audit logging to Azure Blob Storage.

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/taitson/entra-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server