Dataverse Local MCP
Enables interaction with Microsoft Dataverse and Dynamics 365 data through natural language, including CRUD operations, metadata exploration, and saved views.
README
Dataverse Local MCP
Connect Claude (or any MCP client) to your Microsoft Dataverse / Dynamics 365 environment and work with your data in plain English — query records, run your saved views, explore tables and columns, create and update rows.
- Sign in like you always do — your own Microsoft work account, in your browser, with the same trusted sign-in XrmToolBox uses. Works out of the box: no app registration, no API keys, no admin setup.
- Fast from the second call — your environment's schema and saved views are prefetched and cached locally, so metadata questions answer instantly.
- Full toolbox — OData queries, FetchXML (aggregates and joins), entity CRUD, and metadata discovery in one server.
Guides: Setup · User guide
Getting started
1. Install Node.js 18 or newer if you don't have it.
2. Install the server from the npm install page:
npm install -g dataverse-local-mcp
(Or skip the install and use npx -y dataverse-local-mcp as the command below.)
3. Add it to your MCP client — for Claude Desktop, add this to claude_desktop_config.json, replacing the URL with your environment's:
{
"mcpServers": {
"dataverse": {
"command": "dataverse-local-mcp",
"args": ["https://yourorg.crm.dynamics.com"]
}
}
}
4. Sign in once. The first time a tool runs, your browser opens a Microsoft sign-in — pick your work account for that environment. The token is cached at ~/.dataverse-mcp/token-cache.json, so you won't be asked again until it expires. If the browser signs you in with the wrong account, an account picker is always shown so you can switch.
Try it: ask your MCP client to run the whoami tool — it should return your Dataverse UserId and OrganizationId. Then try "list my saved views on account" or "show me the top 5 accounts by name".
Tools
Data
| Tool | What it does |
|---|---|
whoami |
Verify auth: returns UserId, BusinessUnitId, OrganizationId |
get |
Raw OData GET relative to /api/data/v9.2/, e.g. accounts?$select=name&$top=5 |
fetch_xml |
Run a FetchXML query — aggregates, link-entity joins, complex filters; returns formatted values |
create |
Create a record — previews by default, payload validated against cached metadata first |
update |
Update one record by id or a filter matching exactly one row; optimistic concurrency by default |
delete |
Delete one record — preview lists its current values first; permanent |
associate / disassociate |
Relate or unrelate two records through a navigation property |
invoke_action |
Run a bound or unbound action (WinOpportunity, SetState, Field Service booking actions) |
invoke_function |
Run a bound or unbound function — side-effect free, so no confirmation needed |
list_saved_queries |
Browse system and personal saved views — filter by entity, scope, or name substring |
get_saved_query |
One saved view including its FetchXML, by id or name — run or adapt it with fetch_xml |
Schema
| Tool | What it does |
|---|---|
list_tables |
List tables from the local cache — filter by custom/out-of-the-box, name fragment, or solution |
describe_table |
One table in full: columns, types, required levels, option sets, lookup targets, relationships, annotations, sampled fill rates |
find_column |
Search cached columns by name fragment or display label, across every table cached in full |
lookup_reference |
Microsoft Learn documentation for an out-of-the-box table (defers to the Learn MCP when you have it) |
refresh_metadata |
Rebuild the cache, optionally scoped to named tables |
Annotations
| Tool | What it does |
|---|---|
annotate |
Record a local note on a table or column, marked confirmed or inferred |
remove_annotation |
Delete the local note(s) for one target |
export_annotations |
Write the annotation markdown to a path you name |
import_annotations |
Import a markdown file — refused unless its organizationId matches the connected environment |
check_drift |
Resolve every annotation against the current schema: valid, changed, or orphaned |
promote_annotation |
Write a confirmed annotation into the Dataverse description itself — maker mode only |
Environment
| Tool | What it does |
|---|---|
environment_info |
Cache state: org id, mode, last sync, table counts, sampling settings, drift |
set_environment_config |
Set friendly name, mode, out-of-the-box allowlist, table cap, and row sampling |
set_storage |
Choose where documentation and the metadata cache live — local, git, Obsidian, OneDrive, Basic Memory, Notion, or any folder |
Writing safely
Every mutating tool previews by default. Called without confirm: true it describes exactly what would change — naming the resolved record by its primary name — and calls nothing. delete additionally lists the record's current field values, so you can see what is about to be lost.
- One record at a time.
updateanddeletetake an id or awherefilter, and anything matching more than one record is refused with the candidates listed rather than fanning out. - Optimistic concurrency by default. Updates and deletes carry the record's ETag, so a write against a record that changed after you read it fails instead of silently overwriting someone else's work. Pass
concurrency: falseto opt out. - Payloads are checked before they are sent. Unknown columns, columns not valid for the operation, out-of-range option set values, and unknown
@odata.bindnavigation properties all fail locally with a message naming the problem — not an opaque platform 400. - Actions cascade. Much of the real work in Dataverse happens through actions rather than table writes, and their effects reach further than the call suggests. The preview shows the call, not its consequences: those cannot be known without running it.
Errors surface the Dataverse hex code and message, with recognised shapes (privilege denied, duplicate detection, business rule or plugin rejection, concurrency, unknown column) prefixed by a plain-language line. Anything unrecognised passes through verbatim rather than being guessed at.
Resources
The environment's full OData $metadata (CSDL/EDMX) schema is exposed as an MCP resource at dataverse://metadata (application/xml, often several MB).
How the cache works
Right after the stdio handshake the server builds its caches in the background. It never opens a browser at startup: the prefetch uses silent auth only, so with no cached token it waits and retries after your first tool call signs in. Nothing blocks on it — a cold start still works, just slower on the first call.
Everything is keyed by OrganizationId, not environment URL, because URLs change and org ids don't:
~/.dataverse-mcp/
token-cache.json
environments/
index.json # host -> organizationId, so a warm start needs no network
<organizationId>/
config.json # url, friendly name, mode, storage, scope, sampling
schema.json # cached metadata } these two follow
schema.fingerprint # hash for drift detection } your storage choice
annotations.md # your documentation }
metadata.xml # the $metadata resource } always local:
saved-queries.json # } large, derived, cheap to refetch
config.json and index.json always stay local — they hold the storage settings themselves, so they can't live inside the backend they describe.
Scope. Every table gets a cheap name-level summary. Full column and relationship detail is cached for all custom tables plus an allowlist of out-of-the-box ones (Field Service and core sales/service by default), capped at maxFullTables. Anything else is fetched lazily and merged the first time a tool touches it.
Row sampling is off by default. Enable it per environment and the cache also records, per column, the fill rate and up to five example values from at most 20 rows — the single most useful signal for tables whose descriptions are blank. It reads real data, so it stays opt-in, and it never samples columns whose type or format suggests personal data unless you explicitly allow it.
Annotations
Dataverse descriptions are frequently blank. The environment's own shape carries most of the meaning; the rest is human knowledge worth accumulating instead of re-deriving every session. Annotations live in plain markdown at environments/<organizationId>/annotations.md — human-editable, diffable, and safe to check into an engagement repo.
## rsm_cipscenariocandidate
Candidate records for capital improvement plan scenario modelling. Populated by
the scenario engine, not by users directly.
_author: ben.vollmer_ · _added: 2026-08-20_ · _confidence: confirmed_ · _provenance: human_
### rsm_scenariotype
Picklist. 1 = replacement, 2 = rehabilitation, 3 = deferral.
_author: ben.vollmer_ · _added: 2026-08-20_ · _confidence: inferred_ · _provenance: human_
Each annotation carries two independent fields. Confidence is confirmed only when a human stated it; anything a model or tool worked out is inferred — which is exactly why annotations are never written back into Dataverse descriptions by default. Provenance is human, preflight, velocity or model, and governs overwrite behaviour on re-scan: a writer may replace its own earlier note freely, and a human replaces anything, but nothing else overwrites. Where a re-scan contradicts a person's note, both are kept and flagged for a human to settle rather than one silently winning.
Tool-authored notes are rendered as blockquotes so you can tell at a glance what came from a person:
### rsm_scenariotype
> No plugins are registered on this column.
_author: preflight_ · _added: 2026-08-20_ · _confidence: confirmed_ · _provenance: preflight_
Sharing. export_annotations writes the file anywhere you like; import_annotations reads one back. Front matter carries the organizationId, and an import into a different org is refused, never merged. Where both sides annotate the same target with different text, both are kept and flagged rather than one silently winning.
Drift. A fingerprint over the schema is captured at cache time. When it moves, each annotation resolves to valid, changed (the type or option set moved underneath the note) or orphaned (the target is gone). A short summary is logged on connect, the specific warning repeats inline in describe_table, and nothing is ever deleted automatically.
Modes. Set explicitly per environment, never inferred from privileges — privileges are usually broader than intent. consumer (the default) keeps annotations local and never writes metadata. maker will additionally allow promoting a confirmed annotation into the Dataverse description itself.
Promoting documentation into Dataverse
If you own an environment's schema, a confirmed annotation can become the real Dataverse description. This is deliberately awkward and the friction should not be reduced for convenience: it needs maker mode, a confirmed annotation (inferences are refused), no unresolved conflict on it, one target per call, and an explicit confirmation after reading the preview.
The written text is prefixed with a [dataverse-mcp] marker plus provenance and date. That marker is the point — without it a promoted note becomes indistinguishable from a human-authored description six months later, and something that read as a reasonable guess starts reading as fact.
Promotion is a metadata write: it creates an unmanaged customization in the active solution layer, which can mask later updates to a managed component, and publishing may be required before the description appears in the UI. The preview says all of this before you confirm, and the tool cannot undo it.
Where your documentation lives
By default everything sits under ~/.dataverse-mcp. Point it somewhere else with set_storage and both the annotations and the metadata cache follow — they always travel together, per environment.
| Kind | What it does |
|---|---|
local |
Default. Under ~/.dataverse-mcp/environments/<organizationId>/ |
git |
A repo on disk. Every write is committed, so the documentation carries history and diffs; set autoPush to push each commit |
obsidian |
Markdown into your vault — defaults to ~/Obsidian, or give an explicit path |
onedrive |
Into the OneDrive sync folder — $OneDrive or ~/OneDrive |
basic-memory |
Into the Basic Memory notes directory — defaults to ~/basic-memory |
directory |
Any other folder you name |
notion |
The annotation document as a page under a parent page you choose |
The file-backed kinds are one implementation: an Obsidian vault, a OneDrive sync folder and a Basic Memory directory are all just folders, and git adds a commit step. Each environment gets its own subfolder (dataverse-mcp/<friendlyName>-<orgId prefix>) so a shared vault or repo can hold several without collision.
Notion needs an internal integration token in a NOTION_TOKEN environment variable — set it in your MCP client config, not in a file — and a notionPageId for the parent page, which must be shared with your integration. Each markdown line becomes one paragraph block, so the document round-trips exactly and stays readable and editable in Notion. Because Notion is a document store rather than a file store, the schema cache stays on local disk when Notion is selected; the annotations live in Notion.
Switching storage does not copy what you already have — run export_annotations first if you want to carry it across.
Upgrading from 0.3.x
list_entities and describe_entity are replaced by list_tables and describe_table, which read the new cache and fold in annotations and drift warnings. The 0.3.x cache directory ~/.dataverse-mcp/cache/<host>/ is no longer read and can be deleted; the new cache rebuilds itself on first connect. Your token cache is untouched, so no new sign-in is needed.
Build Spec (for contributors)
Goal
Build a standalone MCP server that talks directly to the Dataverse Web API. Going straight to the Web API keeps the server small and dependency-light, and lets it use the sign-in flow that works most broadly across machines and tenants — including tenants with strict Conditional Access policies. TypeScript, local Node host, no new app registration required.
Why this auth approach
This server uses the same proven auth pattern as XrmToolBox and Microsoft's own XRM Tooling samples: a Microsoft-provided, pre-consented public client with a loopback redirect, driven as a standard MSAL auth-code-plus-PKCE flow. It's the ordinary browser sign-in your tenant already trusts — it runs on every OS, satisfies Conditional Access policies that stop device-code flows, and needs no OS-level broker. Anywhere XrmToolBox connects, this connects.
Client ID: 51f81489-12ee-4a9e-aaae-a2591f45987d
Redirect URI: http://localhost
Authority: https://login.microsoftonline.com/common
Scope: <environmentUrl>/.default
This is a Microsoft multi-tenant sample app with user_impersonation delegated permission, no admin consent required. If XrmToolBox already connects successfully in your tenant, this same client ID is proven to already clear Conditional Access there.
Non-goals for v1
- No custom Entra app registration (use the well-known client ID above)
- No service principal / CI auth (interactive user auth only)
Repo layout
packages/
core/ @dataverse-platform/core — shared library, private
src/
index.ts public surface
auth.ts MSAL interactive + silent acquisition
cache.ts atomic read/write helpers
paths.ts ~/.dataverse-mcp layout
environment.ts per-environment config, OrganizationId resolution
dataverseClient.ts Web API calls
writes.ts preview/confirm, validation, single-record resolution
promotion.ts annotation -> Dataverse description, maker mode only
errors.ts Dataverse error translation
store.ts $metadata + saved-view warm cache
metadata/ schema cache: types, fingerprint, build, sampling
annotations/ markdown model, store, drift detection
storage/ backends: directory/git presets, Notion
mcp-server/ dataverse-local-mcp — published to npm
src/
server.ts MCP wiring
tools/ tool definitions and formatters
build.mjs esbuild bundle (inlines core)
prepack.mjs stages README/LICENSE for packing
package.json npm workspaces root
tsconfig.base.json
The assessment tools (powerpreflight, velocity) join as further packages/*, calling core directly as a library rather than going through the MCP server.
Dependencies
npm install # installs every workspace
npm run typecheck # tsc -b across packages
npm run build # core via tsc, mcp-server bundled via esbuild
npm run clean # removes dist and tsbuildinfo
Runtime dependencies are @azure/msal-node, @modelcontextprotocol/sdk and open. HTTP calls use Node's built-in global fetch (hence the Node ≥ 18 requirement) — no HTTP client dependency.
Step 1 — Auth module (src/auth.ts)
Acquire and cache a token using acquireTokenInteractive, which spins up its own loopback listener, no manual HTTP server needed.
- Client ID
51f81489-12ee-4a9e-aaae-a2591f45987d, authorityhttps://login.microsoftonline.com/common - Scope
<environmentUrl>/.default - Token cache persisted to
~/.dataverse-mcp/token-cache.json - Silent acquisition from cache first, fall back to interactive (system browser opened via the
openpackage; setDATAVERSE_MCP_NO_OPEN=1to print the URL instead) - Interactive sign-in always shows the account picker (
prompt: select_account) so browser SSO can't silently hand back the wrong account's token - Concurrent interactive sign-ins are deduped per environment — parallel requests share one browser window
- A
silentOnlymode backs the cache prefetch: it throws instead of opening a browser, so background work never interrupts client startup
Step 2 — Dataverse Web API client (src/dataverseClient.ts)
Thin wrapper over the Dataverse Web API (/api/data/v9.2/) sending Authorization: Bearer, OData-MaxVersion: 4.0, OData-Version: 4.0 headers, retrying 429/503 on Retry-After so a bulk metadata build survives service protection limits. Covers whoAmI(), generic get(), record create/update/delete (PATCH sends If-Match: * so updates never silently upsert), FetchXML queries, saved views (savedquery + userquery, following @odata.nextLink), the raw $metadata EDMX, and metadata reads over EntityDefinitions.
Two Dataverse constraints shape the metadata calls: EntityDefinitions rejects $top and $orderby (it accepts $select and $filter), and DisplayName/Description/RequiredLevel come back as objects rather than scalars, so labels are extracted from UserLocalizedLabel.Label. Option sets need a cast — the client tries the EnumAttributeMetadata base cast (one call for picklist, state, status and multiselect) and falls back to the concrete casts where that isn't supported.
Step 3 — MCP server entry (src/server.ts)
Registers the tools listed in the Tools section above plus the dataverse://metadata resource, and kicks off the background cache prefetch after the transport connects. Uses the standard @modelcontextprotocol/sdk Server class with stdio transport, matching how @microsoft/dataverse mcp itself runs. The environment URL is passed as the first CLI argument.
Step 4 — First test
npm run build
node dist/server.js https://yourorg.crm.dynamics.com
Expected: system browser opens once for interactive sign-in, token caches to ~/.dataverse-mcp/token-cache.json, subsequent runs reuse the cached token silently. Confirm success by calling the whoami tool and checking the returned UserId/BusinessUnitId. After the first sign-in, the background prefetch fills ~/.dataverse-mcp/cache/<org-host>/ with metadata.xml, entities.json, and saved-queries.json; later launches serve metadata and saved-view tools from that cache.
Step 5 — Claude Desktop config
{
"mcpServers": {
"dataverse": {
"command": "node",
"args": ["/full/path/to/DataVerseLocalMCP/dist/server.js", "https://yourorg.crm.dynamics.com"]
}
}
}
Recommended Servers
playwright-mcp
A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.
Magic Component Platform (MCP)
An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.
Audiense Insights MCP Server
Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.
VeyraX MCP
Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.
graphlit-mcp-server
The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.
Kagi MCP Server
An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.
E2B
Using MCP to run code via e2b.
Neon Database
MCP server for interacting with Neon Management API and databases
Exa Search
A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.
Qdrant Server
This repository is an example of how to create a MCP server for Qdrant, a vector search engine.