entra-mcp
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@entra-mcpfind stale accounts inactive for 90 days with their licenses"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
entra-mcp
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 serveThat 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 |
| Who is in the tenant, filtered by department or account state |
|
| One person: profile, manager, groups, licences |
|
| Seats bought vs. seats assigned, per SKU |
|
| Idle accounts and the licences they hold |
|
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 --demoThe 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 ID → App registrations → New 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.AllUnder Certificates & secrets, create a client secret and copy the value immediately — it is shown once.
AuditLog.Read.Allalso requires Microsoft Entra ID P1 or higher on the tenant. Without it,signInActivitydoes not exist in Graph at all andentra_find_stale_accountswill 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 consoleOpens 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 9000The 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--hostexposes an unauthenticated view of your directory, and the server says so when you try.
Audit trail
entra-mcp serve --live --audit-trail ./entra-mcp.jsonlOne 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 pytest99 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, serialisetools.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 --demopython.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.3 — entra_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 toolsentra_find_stale_accountsARead-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.
| Name | Required | Description | Default |
|---|---|---|---|
| days | No | Minimum days since the last successful sign-in for an account to count as stale. | |
| licensed_only | No | Only accounts holding at least one licence. True answers 'what are we paying for'; false answers 'what should be cleaned up'. | |
| include_never_signed_in | No | Include accounts with no recorded sign-in. Turn off to filter out service accounts, which never sign in interactively. |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
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.
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.
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.
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.
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.
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_usageARead-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.
| Name | Required | Description | Default |
|---|---|---|---|
No parameters | |||
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
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.
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.
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.
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.
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.
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_userARead-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.
| Name | Required | Description | Default |
|---|---|---|---|
| user | Yes | User principal name (adele.vance@contoso.com) or object id. |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
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.
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.
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.
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.
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.
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_usersARead-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.
| Name | Required | Description | Default |
|---|---|---|---|
| limit | No | Maximum accounts to return. | |
| offset | No | Accounts to skip, for paging through a large tenant. | |
| department | No | Only accounts in this department, matched exactly (e.g. 'Finance', 'Engineering'). | |
| enabled_only | No | Exclude disabled accounts. Leave false when auditing licence waste — disabled accounts are the ones most likely to be holding one. |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
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.
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.
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.
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.
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.
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
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.
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.
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.
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
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
Safe, read-only Postgres and MySQL access for AI agents. Audit log + column-level controls.
Read-only finance and operations controls for AI agents with evidence and safe next actions.
Permissioned access to Outlook, OneDrive and Teams via the user's own Microsoft account
Credential broker for AI agents: scoped, revocable API access with policy enforcement and audit.
Related MCP Servers
- -licenseNot gradedqualityNot gradedmaintenanceProvides 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.
- AlicenseNot gradedqualityCmaintenanceEnables AI assistants and automation tools to manage Microsoft 365, Entra ID, and Intune resources through 32 tools for user/device/file management and infrastructure monitoring.6MIT
- AlicenseNot gradedqualityDmaintenanceEnables AI agents to query Microsoft Entra data using natural language, converting requests into Microsoft Graph API calls for read-only enterprise IT scenarios.52CC BY-4.0
- AlicenseAqualityBmaintenanceEnables auditing and monitoring of Microsoft Entra ID security posture, Conditional Access policies, and Zero Trust alignment via Microsoft Graph API.5MIT
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
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