dataverse-mcp-server
Use this MCP server to browse, query, and modify Microsoft Dataverse/Dynamics 365 data and schema, with destructive deletes disabled by default.
List Dataverse tables and solutions, with optional prefix/solution filters.
Inspect table schemas, choice columns, global/local OptionSets, and alternate keys.
Query records with OData options ($filter, $select, $top, $orderby, $expand) and retrieve records by ID.
Create and update records; delete records only when
DATAVERSE_ALLOW_DELETE=trueis set.Invoke Dataverse Web API actions (POST) and functions (GET), bound or unbound.
Create tables, add/update/delete columns, create 1:N or N:N relationships, and inspect column dependencies.
Create, list, and delete alternate keys for uniqueness and keyed-PATCH upserts.
Read, add, update, and delete picklist options on Local or Global OptionSets.
Use solution filtering only for
list_entities; data tools retain access to any table regardless of solution membership.
Click on "Deploy 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., "@dataverse-mcp-server Show me all accounts created last week"
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.
dataverse-mcp-server
MCP (Model Context Protocol) server for Microsoft Dataverse API with safe-by-default configuration. Works with any Dataverse / Dynamics 365 environment.
Tools
Data operations
Tool | Description |
| List Dataverse tables with optional prefix and solution filters |
| List Dataverse solutions (use |
| Get attributes of a specific table — choice columns carry an |
| Query records with OData $filter, $select, $top, $orderby, $expand |
| Get a single record by ID |
| Create a record |
| Update a record |
| Delete a record (disabled by default, see Safety) |
Note:
solution/DATAVERSE_SOLUTION_NAMEonly scopeslist_entities(schema browsing). Data tools (query_records,get_record,create_record, …) keep full access to any table regardless of solution membership — shared tables likeaccountorcontactremain reachable.
Schema operations
Tool | Description |
| Create a new table with attributes |
| Add a column to an existing table (Choice columns can bind to a Global OptionSet) |
| Update column metadata (display name, required level, bounds, …) |
| Delete a column (disabled by default, see Safety) |
| List CRM components (forms, views, workflows, …) that reference a column — use after |
| Create relationships between tables (1:N, N:N) |
| List alternate keys on a table (returns |
| Create an alternate key (single or composite) — enables race-safe keyed-PATCH upserts |
| Delete an alternate key and its supporting unique index (disabled by default, see Safety) |
Dataverse does not allow changing a column's logical name or type. To "rename" or change type: create a new column, migrate data via
update_record, thendelete_attributeon the old one.
Choice columns: local values or a shared Global OptionSet
A Picklist attribute takes exactly one of two fields. options defines the values inline and produces a Local OptionSet owned by that single column:
{ "logical_name": "contoso_source", "type": "Picklist", "display_name": "Source",
"options": [ { "label": "Website", "value": 909890000 } ] }global_option_set instead binds the column to an existing Global OptionSet by name, so several columns across several tables share one list and cannot drift apart:
{ "logical_name": "contoso_source", "type": "Picklist", "display_name": "Source",
"global_option_set": "contoso_sourceset" }Supplying both is rejected. That check is not cosmetic: Dataverse itself accepts the pair and then silently ignores the binding, leaving a local copy that looks bound. An unknown set name fails as Global OptionSet not found: '<name>' before anything is created — including in create_entity, which resolves names and validates every attribute before the table exists, so a rejected column cannot leave a half-built table behind.
Verify the result with get_picklist_options: a bound column reports is_global: true and the global set's own metadata_id.
Picklist option management
Tool | Description |
| Read a Local or Global OptionSet — its identity plus |
| Add an option to an existing OptionSet ( |
| Rename an option on an OptionSet ( |
| Remove an option from an OptionSet ( |
Picklist tools accept either entity_logical_name + attribute_logical_name (a column) or option_set_name (a Global OptionSet) — the two modes are mutually exclusive. Write operations require Customizer or System Administrator role on the connected service principal. Deleting an option does not update existing records that hold its numeric value — they are left with an orphan integer.
Telling a Global OptionSet from a local copy
get_picklist_options returns the set's identity alongside its options:
{
"option_set": {
"name": "fundai_source",
"is_global": true, // bound to a shared Global OptionSet
"metadata_id": "ea6ab542-9c2e-f111-88b3-00224805d253"
},
"options": [ { "value": 909890000, "label": "Website" }, /* … */ ]
}is_global is the answer to "does this column reuse an org-wide list, or does it own a private copy?" — matching values prove nothing on their own, and a column with a local set reports an auto-generated name like opportunity_prioritycode with is_global: false. To confirm which global set a column is bound to, compare its metadata_id against the one returned by get_picklist_options { option_set_name: … }.
The lookup covers Choice, Status, State and MultiSelect columns, so statecode / statuscode can be read the same way as a custom choice column.
get_entity_schema reports the same identity per column as a compact option_set summary with an option_count instead of the values themselves — read the values for a single column with get_picklist_options:
{
"LogicalName": "fundai_source",
"AttributeType": "Picklist",
"option_set": { "name": "fundai_source", "is_global": true, "metadata_id": "ea6ab542-…", "option_count": 6 }
}Actions & functions
Tool | Description |
| Invoke a Web API action (POST), bound or unbound — for operations outside plain CRUD (e.g. |
| Invoke a Web API function (GET), bound or unbound — read-only operations exposed as functions (e.g. |
Pass entity_set + id for a bound call (POST /<entity_set>(<id>)/Microsoft.Dynamics.CRM.<name>); omit both for an unbound call (POST /<name>). For invoke_action, parameters is the JSON request body; for invoke_function, parameters is inlined as OData function arguments. Bare operation names are namespaced automatically for bound calls — pass a fully-qualified name to override.
Examples:
// Publish a draft duplicate-detection rule.
// PublishDuplicateRule is a BOUND action on duplicaterule (returns an async job).
invoke_action({ name: "PublishDuplicateRule", entity_set: "duplicaterules", id: "<guid>" })
// Unpublish is an UNBOUND action taking DuplicateRuleId — note the asymmetry.
invoke_action({ name: "UnpublishDuplicateRule", parameters: { DuplicateRuleId: "<guid>" } })
// Qualify a lead into Account/Contact/Opportunity (bound action on lead).
invoke_action({ name: "QualifyLead", entity_set: "leads", id: "<guid>",
parameters: { CreateAccount: true, CreateContact: true, CreateOpportunity: true, Status: 3 } })Whether an operation is bound or unbound is defined in the Web API
$metadata, not by intuition — e.g.PublishDuplicateRuleis bound butUnpublishDuplicateRuleis unbound. Check$metadata(look forIsBound="true"and the bindingParameter) if a call returns404 "Resource not found for the segment".
⚠️
invoke_actioncan perform arbitrary mutating operations. It is currently ungated by design; capability-based access control (a safe-by-default policy gating writes/actions) is tracked separately in #45 / #46.invoke_functionis read-only.
Related MCP server: xrm-mcp
Quick start (no clone)
Add to .mcp.json in your project root:
{
"mcpServers": {
"dataverse": {
"command": "npx",
"args": ["-y", "@rededis/dataverse-mcp-server"]
}
}
}Create a .env file next to it with the four required variables (see Environment variables below) and restart your MCP client. The -y flag tells npx to auto-confirm the package install.
Setup
Environment variables
DATAVERSE_TENANT_ID=your-azure-tenant-id
DATAVERSE_CLIENT_ID=your-app-registration-client-id
DATAVERSE_CLIENT_SECRET=your-client-secret
DATAVERSE_RESOURCE_URL=https://your-org.crm.dynamics.com
DATAVERSE_ENTITY_PREFIX=contoso_ # optional, default prefix filter for list_entities
DATAVERSE_SOLUTION_NAME=MySolution # optional, default solution unique name for list_entities
DATAVERSE_ALLOW_DELETE=true # optional, enable delete operations (disabled by default)
DATAVERSE_REQUEST_TIMEOUT_MS=30000 # optional, per-request timeout in ms for Dataverse and token calls (default 30000, max 120000)
DATAVERSE_MAX_CONCURRENCY=8 # optional, requests sent to Dataverse at once (default 8, 1 to 100)
DATAVERSE_MAX_QUEUE_LENGTH=100 # optional, requests that may wait for a free slot (default 100, 0 to 10000)
DATAVERSE_MAX_QUEUE_WAIT_MS=10000 # optional, longest wait for a free slot in ms (default 10000, max 120000)
DATAVERSE_MAX_ATTEMPTS=3 # optional, times a throttled request is sent, the first try included (default 3, 1 to 10)
DATAVERSE_MAX_RETRY_WAIT_MS=15000 # optional, longest wait on throttling per request in ms (default 15000, max 120000)DATAVERSE_REQUEST_TIMEOUT_MS bounds how long a tool call waits for Dataverse. Dataverse itself cancels any operation after 2 minutes, hence the maximum. Schema changes such as create_entity with many columns can take longer than the 30-second default. When that happens the tool reports a timeout, but Dataverse still finishes the change, so check before retrying, or raise the value.
Service protection limits
Dataverse throttles each user with service protection limits and answers 429 Too Many Requests when one is reached. Parallel tool calls can reach them even from one machine. The server handles this in two ways, and all five DATAVERSE_MAX_* variables above are optional:
It limits what it sends. At most
DATAVERSE_MAX_CONCURRENCYrequests are in flight; the rest wait in arrival order. A request is turned away with a "Dataverse is busy" error whenDATAVERSE_MAX_QUEUE_LENGTHrequests are already waiting, or when it has waitedDATAVERSE_MAX_QUEUE_WAIT_MSwithout getting a slot.It retries a 429. The server waits for as long as the
Retry-Afterheader says, then resends, up toDATAVERSE_MAX_ATTEMPTSsends in total. While it waits, no other request is sent either, because Dataverse extends the wait for a client that keeps sending. Other requests are held back for the wait Dataverse asked for, but never longer thanDATAVERSE_MAX_RETRY_WAIT_MS: aRetry-Afterof several minutes fails the call that received it and does not stop the rest for that long. If the waits of one request would add up to more thanDATAVERSE_MAX_RETRY_WAIT_MS, the tool call fails at once with an error that says in how many seconds to retry.
With the defaults, these limits add at most 25 seconds of waiting to one request: 10 in the queue and 15 on throttling. With one request at the default 30-second timeout that makes 55 seconds, inside the 60 seconds a client built on the MCP TypeScript SDK waits for an answer unless configured otherwise. That figure assumes a 429 arrives promptly, and it does not always: in a live test, heavy requests that hit the execution-time limit were answered with 429 only after 28 to 75 seconds. The bound that holds whatever happens is 115 seconds: the 25 seconds of waiting plus three sends that each take the whole request timeout. A request that slow is cut off by DATAVERSE_REQUEST_TIMEOUT_MS first and reported as a timeout, not retried. A tool that sends several requests can take that long for each of them. If you raise the waits or DATAVERSE_REQUEST_TIMEOUT_MS, raise the tool-call timeout of your MCP client to match.
The concurrency limit alone does not prevent throttling, since Dataverse also limits combined execution time. The defaults are deliberately conservative choices of this server, not values published by Microsoft. A value that is set but out of range is reported through dataverse_setup, like a missing variable.
Azure App Registration
Register an app in Azure AD
Add API permission: Dynamics CRM > user_impersonation (or Application permissions)
Create a client secret
Grant the app a security role in Dataverse (e.g. System Administrator for full access)
Build
npm install
npm run buildnpm run check is what the pre-commit hook and CI run. The pre-push hook scans the commits being pushed for environment hosts and tenant or client IDs with Betterleaks, if it is installed (brew install betterleaks, or the release binary); without it the push goes through with a warning and CI runs the scan. A git client that runs hooks with its own PATH may not find an installed Betterleaks; husky reads ~/.config/husky/init.sh, where PATH can be set.
Claude Code configuration (local build)
If you cloned the repo instead of using npx:
{
"mcpServers": {
"dataverse": {
"command": "node",
"args": ["./dist/index.js"]
}
}
}Create a .env file with your credentials (see .env.example).
Safety
Destructive operations are disabled by default to prevent accidental data loss. All four delete tools are gated behind the same DATAVERSE_ALLOW_DELETE=true flag:
delete_record— removes a row and all its datadelete_attribute— removes a column along with ALL values across every record (no recovery short of a full environment restore)delete_picklist_option— removes an option from an OptionSet; records that hold the option's integer value are left with an orphan number (no label in UI, broken reports)delete_entity_key— drops an alternate key and its supporting unique index; any keyed-PATCH upsert flows relying on it stop working
When the flag is off, each tool registers as a stub that returns an instructional error instead of performing the delete. To enable, add DATAVERSE_ALLOW_DELETE=true to your .env file and restart the MCP server.
License
MIT
Available Tools
23 toolsadd_attributeC
Add a column (attribute) to an existing Dataverse table
Inputs: attribute (required), entity_logical_name (required).
add_entity_keyA
Create an alternate key on a Dataverse table (composite supported via key_attributes). Use for race-safe upserts via keyed-PATCH or to enforce a uniqueness constraint that the primary key doesn't cover. NOTE: Dataverse builds the supporting unique index asynchronously — the key is not usable for keyed lookups until its EntityKeyIndexStatus becomes 'Active'. Poll with list_entity_keys.
Inputs: display_name (required), logical_name (required), key_attributes (required), entity_logical_name (required), +1 more.
add_picklist_optionA
Add an option to an existing Local or Global OptionSet (Dataverse InsertOptionValue action). Requires Customizer or System Administrator role; HTTP 403 otherwise.
Inputs: label (required), value, description, language_code, +4 more.
create_entityC
Create a new Dataverse table (entity) with specified attributes
Inputs: display_name (required), logical_name (required), display_collection_name (required), has_notes, +6 more.
create_recordC
Create a new record in a Dataverse table
Inputs: data (required), entity_set (required).
create_relationshipC
Create a relationship between two Dataverse tables
Inputs: type (required), schema_name (required), primary_entity (required), related_entity (required), +2 more.
delete_attributeA
Delete a column from a Dataverse table (currently disabled for safety)
Inputs: entity_logical_name (required), attribute_logical_name (required).
delete_entity_keyC
Delete an alternate key from a Dataverse table (currently disabled for safety)
Inputs: key_logical_name (required), entity_logical_name (required).
delete_picklist_optionA
Remove an option from a Local or Global OptionSet (currently disabled for safety)
Inputs: value (required), option_set_name, entity_logical_name, solution_unique_name, +1 more.
delete_recordB
Delete a record from a Dataverse table (currently disabled for safety)
Inputs: id (required), entity_set (required).
get_attribute_dependenciesA
List CRM components that reference a column — forms, views, workflows, business rules, plugins. Call this when delete_attribute fails with 0x8004f01f, or before any destructive change to a column. Component names are best-effort: resolved for common types, null otherwise. Backed by the Dataverse RetrieveDependenciesForDelete function.
Inputs: entity_logical_name (required), attribute_logical_name (required).
get_entity_schemaA
Get attributes (columns) of a specific Dataverse table. Choice-style columns (Choice, Status, State, MultiSelect) carry an option_set summary with is_global and option_count, so one dump shows which choice lists are shared org-wide. Read the option values per column with get_picklist_options.
Inputs: entity_logical_name (required).
get_picklist_optionsB
Read a Local or Global OptionSet as { option_set: { name, is_global, metadata_id }, options: [{ value, label }] }. Use is_global to tell whether a column holds a local copy of the values or is bound to a shared Global OptionSet — matching values alone do not prove a binding. Works for Choice, Status, State and MultiSelect columns.
Inputs: option_set_name, entity_logical_name, attribute_logical_name.
get_recordC
Get a single record by ID from a Dataverse table
Inputs: id (required), entity_set (required), expand, select.
invoke_actionA
Invoke a Dataverse Web API action (POST) — bound or unbound. Use for operations that are not plain CRUD, e.g. PublishDuplicateRule (bound to a duplicaterule) or QualifyLead (bound to a lead), or UnpublishDuplicateRule (unbound, takes DuplicateRuleId). Whether an action is bound is defined in the Web API $metadata. Pass entity_set+id for bound actions, neither for unbound. parameters becomes the JSON request body.
Inputs: name (required), id, entity_set, parameters.
invoke_functionA
Invoke a Dataverse Web API function (GET) — bound or unbound. Use for read-only operations exposed as functions, e.g. WhoAmI (unbound) or RetrieveDuplicates. Pass entity_set+id for bound functions, neither for unbound. parameters are inlined into the URL as OData function arguments.
Inputs: name (required), id, entity_set, parameters.
list_entitiesC
List Dataverse tables (entities) with optional prefix and solution filters
Inputs: prefix, solution.
list_entity_keysA
List alternate keys defined on a Dataverse table. Returns a flat array of { logical_name, schema_name, display_name, key_attributes, entity_key_index_status, metadata_id }. entity_key_index_status reflects the background index build (Pending → Active, or Failed) — alt keys are not usable for keyed-PATCH upserts until Active.
Inputs: entity_logical_name (required).
list_solutionsB
List Dataverse solutions (uniquename is used to filter list_entities)
Inputs: include_managed.
query_recordsC
Query records from a Dataverse table with OData filters
Inputs: entity_set (required), top, expand, filter, +2 more.
update_attributeA
Update metadata of an existing column: display name, description, required level, max length, min/max value, precision. Dataverse fixes a column's type and logical name at creation — to change either, add_attribute a new column, migrate the values with update_record, then delete_attribute the old one.
Inputs: type (required), entity_logical_name (required), attribute_logical_name (required), required, +10 more.
update_picklist_optionA
Update an existing option's label/description on a Local or Global OptionSet (Dataverse UpdateOptionValue action). Requires Customizer or System Administrator role.
Inputs: label (required), value (required), description, merge_labels, +5 more.
update_recordC
Update an existing record in a Dataverse table
Inputs: id (required), data (required), entity_set (required).
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 23 tools
Most tools are cleanly separated by resource + action (entity vs record vs attribute vs key vs picklist), and generic escape hatches (invoke_action vs invoke_function) are distinguished by POST/GET semantics. Minor overlap remains between get_entity_schema and get_picklist_options (schema dump already summarizes option sets), and create_entity vs create_record could briefly confuse newcomers to Dataverse terminology.
Every tool follows a consistent snake_case verb_noun pattern (list_entities, create_record, update_attribute, delete_picklist_option, get_attribute_dependencies). The only deviation is 'query_records' using a different read verb, but it remains within the same convention and clearly signals a filter-based read versus a list.
23 tools is on the heavy side but is justified by the genuinely broad domain: records, tables, columns, alternate keys, option sets, relationships, solutions, plus two escape hatches. There is little obvious redundancy, though the set is at the upper edge of comfortable before it becomes unwieldy.
Record CRUD and attribute/key/picklist lifecycles are largely covered, but there are notable gaps: relationships can be created but not listed or deleted, tables can be created but not updated or deleted, and solutions are read-only. Several delete operations (record, entity key, picklist option, attribute) are explicitly disabled for safety, which will leave agents at dead ends when cleanup is required.
Maintenance
Related MCP Connectors
The official MCP Server from Mia-Platform to interact with Mia-Platform Console
MCP server for Codat — companies, connections, invoices, bills and financial statements.
MCP server for mandates, delegation, policy-gated execution, credential grants, and audit.
An MCP server that let you interact with Cycloid.io Internal Development Portal and Platform
Related MCP Servers
- AlicenseNot gradedqualityBmaintenanceThe most complete MCP server for Microsoft Dataverse.218 npm18MIT
- FlicenseAqualityCmaintenanceA minimal MCP server that gives AI coding agents clean read and write access to Microsoft Dataverse environments via the Dataverse Web API v9.2. It works as a drop-in alternative to Microsoft's own MCP server, without requiring Copilot Credits or managed environments.821-
- AlicenseAqualityCmaintenanceModel Context Protocol (MCP) server for Microsoft Dynamics 365 Business Central. Provides AI assistants with direct access to Business Central data through properly formatted API v2.0 calls.62,547 npm10MIT
- AlicenseNot gradedqualityDmaintenanceAn MCP server for OData v4 endpoints, especially Microsoft Dataverse/Dynamics 365, enabling authentication, schema discovery, querying, CRUD, and more via natural language.MIT