The full upstream README, mirrored here for reference. Install config, tool schemas, adoption signals, and an original overview live on the Graph Atlas MCP listing page.
An MCP server that tracks schema changes across the Microsoft Graph API — both the changes Microsoft documents and the ones it doesn't. Covers the full Entra ID, Entra ID Governance, Identity & Access Management, Entra Agent ID, and Information Protection surface, seeded with a year of history at launch. Ships with an interactive schema visualiser rendered inside the MCP client via MCP Apps.

Microsoft Graph evolves continuously across v1.0 and beta. The official changelog is curated and incomplete — undocumented schema changes (new properties, removed relationships, new enum values) land in production before they're announced. This MCP closes that gap by diffing the actual $metadata CSDL daily, and backfills a year of history from both a public community tracker and the official changelog.
| Source | What it is | Granularity |
|---|---|---|
seed-entra-ms | One-time import of changes.entra.ms's historical CSDL diffs | Property-level |
backfill-graph-changelog | One-time scrape of Microsoft's official "What's New" history, classified into 8 IAM object families | Feature-level |
self | Daily $metadata fetch + diff, ongoing from first collection | Property-level |
Two further tables enrich every change with real-world permission/role context: permissions (1,036 scopes scraped from Merill's Graph Permissions Explorer) and roles (135 Entra built-in roles and their microsoft.directory/* actions, from the Microsoft Learn permissions reference), cross-referenced into role_permission_map.
Requires Node.js 22+ (uses the built-in node:sqlite). Add to your MCP client — e.g.
Claude Desktop's claude_desktop_config.json:
That's the whole setup: on first launch the server auto-downloads the latest published
database (change history, permissions, roles, embeddings) from this repo's GitHub Releases
into ~/.graph-atlas-mcp/, and keeps it current against the daily release cadence.
This produces graph-atlas.db in the project root. npm run seed uses a gitignored,
one-off local script (scripts/seed-from-entra-ms.js) — it isn't part of the committed repo.
The DB uses WAL journal mode so these collection scripts can run concurrently with an MCP client that already has the file open (e.g. Claude Desktop) without lock contention.
search_changes runs in hybrid (keyword + semantic) mode automatically if OPENAI_API_KEY
is set, and degrades to keyword-only otherwise:
Re-running npm run embed after a collect only embeds new records — it's incremental, not a full re-embed.
When developing, point the client at your build and local DB instead of the npm package:
GRAPH_ATLAS_DB points the server at an explicit DB file. Without it, the server looks for
~/.graph-atlas-mcp/graph-atlas.db (auto-downloaded from the latest GitHub Release), then
falls back to ./graph-atlas.db in the current working directory.
| Tool | Purpose |
|---|---|
search_changes | Natural-language or keyword search across all sources. Hybrid (keyword + semantic via Reciprocal Rank Fusion) when an OpenAI key is configured, keyword-only otherwise. |
get_recent_changes | Structured query — filter by date, endpoint, object type/name, change kind, source. |
get_object_history | Full change history for one object (e.g. group, accessPackage), oldest to newest. Matches case-insensitively across sources. |
get_change_detail | Full detail for a single change record, plus related changes from the same day/object. |
get_snapshot_summary | Entity/property/enum counts per snapshot — "how big is Graph API right now?" |
get_server_info | About the server itself — running version, changelog entry, data freshness, and the two release channels (daily calendar-tagged database releases vs semver npm server releases). |
get_permission_context | Which permissions touch a Graph object, what each unlocks, who can grant them (heuristic — see limitations), and its recent changes. |
schema_change_report | Structured JSON (no UI) of the schema/permission/role graph — nodes + edges + change-activity counts, for reasoning over. |
visualize_schema_graph | Opens the interactive Graph Atlas visualiser inside the MCP client (via MCP Apps) — a D3 force-directed graph with search, expand, and per-node detail. See below. |
expand_schema_node | Expands one node of the open visualiser (or feeds the model a one-node neighborhood delta) — powers the app's double-click/Expand interactions. |
search_schema_objects | Name search across entity types, permissions, and roles — resolves fuzzy names to canonical node ids; powers the app's search box. |
get_node_timeline | App-only: compact change timeline for the visualiser's detail panel (the model uses get_object_history instead). |
visualize_schema_graph opens the Graph Atlas visualiser — a d3 force-directed SVG graph rendered
directly inside the MCP client via MCP Apps
(Claude Desktop, ChatGPT, VS Code, and other compliant hosts). Its architecture and look & feel are
ported from EntraPulse Polyarchy:
search_schema_objects) matches entity types,
permissions, and roles by name and flips focus to your pick.get_node_timeline).updateModelContext after every focus/expansion, so the
assistant knows what's on screen without extra tool calls.Three pivot dimensions, chosen via view (or the in-app tabs) so you can start wherever the
question starts — a Role, a Permission, or an entity/API:
entity — Graph entity types; edges are navigation properties and inheritance.
focus_object = an entity like group. Expanding an entity on the Permission tab reveals the
permissions that touch it; on the Role tab it goes two hops — those permissions plus the
roles that grant them (so expanding user surfaces User Administrator, Helpdesk
Administrator, and friends).permission — a permission scope, the entities it touches, and the roles that grant it.
focus_object = a permission like User.Invite.All.role — an Entra role, the permissions it grants, and the entities those touch (two hops).
focus_object = a role like User Administrator. This is the one that answers "what can this
role actually do" / least-privilege comparison questions.Build it with npm run build (server tsc, an app-ui typecheck, then esbuild bundles
app-ui/atlas-app/ into a single self-contained dist/app-ui/atlas-app.html — MCP App resources
must be one blob with no external network dependencies; the resource URI is
ui://graph-atlas/atlas-app.html). The server reads the built HTML once at startup, so restart
your MCP client (or the server connection) after rebuilding.
For source checkouts all paths must be absolute — MCP Jam spawns the server from its own
working directory. Two
MCP Jam (v2.34) quirks to know about: widgets only render under the MCP Jam host profile
(the "Claude" host-emulation profile leaves the widget iframe stuck at "loading"), and the
widget's Sandbox tab / debug/widget-visibility trace events are the fastest way to diagnose a
blank widget.
Note: entity node ids are lowercase everywhere (accessreview), with display labels keeping CSDL
casing (accessReview) — entity names aren't consistently cased across the CSDL and the
permission source data, and a canonical id is what lets one entity stay one node across all views.
Real bugs caught and fixed while building this:
graph. namespace alias wasn't stripped (silently produced zero edges).permissions.resources entity names aren't consistently cased across Merill's pages (would have
silently split one entity into two duplicate-looking nodes with a dangling edge between them).autoResize (on by default) sizes the iframe off the app's own document content
height — useless for a full-height flex app. The app now disables it and claims space explicitly
(fullscreen where supported, a tall inline frame otherwise).zoomToFit synchronously after
graphData() — before the debounced layout had assigned node positions — producing a NaN zoom
transform and a permanently blank canvas on every re-render after the first. The d3/SVG port
eliminates the bug class: the simulation is synchronous and centering guards unplaced nodes."30") where the server compares ISO dates
lexicographically, silently zeroing every change count. since now accepts both (day counts are
normalized server-side) and the UI converts to ISO dates anyway.requestAnimationFrame in hidden iframes, so a graph seeded while the host had
the widget hidden never got laid out by d3's simulation — every node rendered stacked at the
origin. New nodes now get explicit starting positions next to their expansion source, every
structural render ticks the layout synchronously once, and an IntersectionObserver reheats the
simulation when the canvas becomes visible again.autoResize off, some hosts (Claude Desktop's app surface) still size the iframe from the
app's reported height — the app now always reports a size after display-mode negotiation, using
the host's containerDimensions when published, and re-reports on host-context changes..github/workflows/collect.yml, 02:00 UTC) fetches $metadata for both
endpoints — both are publicly accessible, no Entra app registration required.scripts/parse-csdl.js parses the CSDL XML into structured JSON (entity/complex/enum types,
properties, navigation properties, entity sets, singletons, functions/actions).scripts/diff-snapshots.js compares today's parse against the most recent stored snapshot
in snapshots/v1.0/ and snapshots/beta/ (committed to the repo as the audit trail).graph-atlas.db with source = 'self'; if any changes
were found, scripts/create-release.js publishes an updated GitHub Release.LIKE-based matching, not real BM25 — Node's bundled node:sqlite
doesn't ship the FTS5 extension. Ranking is a simple term-coverage/frequency heuristic.OPENAI_API_KEY; vector storage uses sqlite-vec (bundled, no external vector DB).$metadata diffing covers schema-level changes only — not behavioral, permission, or
endpoint-level changes that don't show up in the CSDL itself.get_permission_context caps response size. A common object like user matches 190+
permissions, and broad permissions like Directory.ReadWrite.All list 300+ endpoints — an
uncapped response hit 417KB in testing and caused a real MCP transport disconnect. Object-name
queries are capped to 25 permissions (narrowest-first by resource count, limit param up to
50) with total_matching_count telling you if more exist; each permission's graph_endpoints
is capped to 10 with graph_endpoints_total_count/graph_endpoints_truncated. Filter to a
specific permission_name to get one permission's full, untruncated detail.grantable_by (role<->permission mapping) is a heuristic correlation, not an authoritative
mapping. Microsoft doesn't publish an official crosswalk between Entra RBAC actions
(microsoft.directory/*) and OAuth permission scopes (User.Read.All, etc.) — they're two
separate authorization systems that happen to govern overlapping resource types.
scripts/build-role-permission-map.js matches a permission's primary resource (derived from
its own name, e.g. AdministrativeUnit.Read.All -> administrativeUnit) against roles' actions
on that same resource. Verify grantable_by results before treating them as authoritative.
Matching is by operation category (read/create/update/delete/restore/enable/disable/invite/
license/session/all), not a coarse read-vs-write binary — an earlier binary version produced a
confirmed false positive ("Directory Writers" has create/update/enable/disable actions on
users but no delete/restore action, yet appeared grantable_by for User.DeleteRestore.All
simply because both were "write-tier"). Graph's own granular permission names
(DeleteRestore, EnableDisableAccount, Invite, ReadUpdate, ...) are operation-specific by
design, so permissionRequiredCategories() maps each verb segment to the specific action
categories that satisfy it — verified against Microsoft's built-in roles reference across the
full guest-lifecycle operation set (invite/create/update/enable-disable/delete-restore ×
Guest Inviter/Directory Writers/User Administrator, all 15 combinations exact). Unrecognized/
long-tail verbs (ReadWrite, Write, Manage, FullControl, and ~75 rarer Teams/Chat-specific
variants) fall back to "any mutate action satisfies" — a broad permission genuinely is satisfied
by partial write capability, so this remains a coarser signal for those specific verbs.GET /roleManagement/directory/roleDefinitions) requires an app
registration with RoleManagement.Read.Directory + Application.Read.All that this project
doesn't have configured — collect-roles.js uses the PRD's documented fallback (scraping the
public Microsoft Learn permissions reference) instead.permissions.combined_with (some endpoints require multiple scopes together, e.g.
Application.Read.All and Policy.Read.All) isn't populated — Merill's page structure doesn't
expose this reliably enough to scrape.This project builds on data and design work from others in the Entra community — credited here as data sources and inspiration, not as project co-authors:
MIT