Skip to main content
Glama
dataverse-mcp-server

dataverse-mcp-server

by rededis

dataverse-mcp-server

npm version npm downloads CI Node.js TypeScript License: MIT

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_entities

List Dataverse tables with optional prefix and solution filters

list_solutions

List Dataverse solutions (use uniquename to filter list_entities)

get_entity_schema

Get attributes of a specific table — choice columns carry an option_set summary

query_records

Query records with OData $filter, $select, $top, $orderby, $expand

get_record

Get a single record by ID

create_record

Create a record

update_record

Update a record

delete_record

Delete a record (disabled by default, see Safety)

Note: solution / DATAVERSE_SOLUTION_NAME only scopes list_entities (schema browsing). Data tools (query_records, get_record, create_record, …) keep full access to any table regardless of solution membership — shared tables like account or contact remain reachable.

Schema operations

Tool

Description

create_entity

Create a new table with attributes

add_attribute

Add a column to an existing table (Choice columns can bind to a Global OptionSet)

update_attribute

Update column metadata (display name, required level, bounds, …)

delete_attribute

Delete a column (disabled by default, see Safety)

get_attribute_dependencies

List CRM components (forms, views, workflows, …) that reference a column — use after delete_attribute fails with 0x8004f01f

create_relationship

Create relationships between tables (1:N, N:N)

list_entity_keys

List alternate keys on a table (returns key_attributes, entity_key_index_status, …)

add_entity_key

Create an alternate key (single or composite) — enables race-safe keyed-PATCH upserts

delete_entity_key

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, then delete_attribute on 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

get_picklist_options

Read a Local or Global OptionSet — its identity plus [{ value, label }]

add_picklist_option

Add an option to an existing OptionSet (InsertOptionValue)

update_picklist_option

Rename an option on an OptionSet (UpdateOptionValue)

delete_picklist_option

Remove an option from an OptionSet (DeleteOptionValue)

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_action

Invoke a Web API action (POST), bound or unbound — for operations outside plain CRUD (e.g. PublishDuplicateRule, QualifyLead)

invoke_function

Invoke a Web API function (GET), bound or unbound — read-only operations exposed as functions (e.g. WhoAmI)

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. PublishDuplicateRule is bound but UnpublishDuplicateRule is unbound. Check $metadata (look for IsBound="true" and the binding Parameter) if a call returns 404 "Resource not found for the segment".

⚠️ invoke_action can 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_function is 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_CONCURRENCY requests are in flight; the rest wait in arrival order. A request is turned away with a "Dataverse is busy" error when DATAVERSE_MAX_QUEUE_LENGTH requests are already waiting, or when it has waited DATAVERSE_MAX_QUEUE_WAIT_MS without getting a slot.

  • It retries a 429. The server waits for as long as the Retry-After header says, then resends, up to DATAVERSE_MAX_ATTEMPTS sends 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 than DATAVERSE_MAX_RETRY_WAIT_MS: a Retry-After of 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 than DATAVERSE_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

  1. Register an app in Azure AD

  2. Add API permission: Dynamics CRM > user_impersonation (or Application permissions)

  3. Create a client secret

  4. Grant the app a security role in Dataverse (e.g. System Administrator for full access)

Build

npm install
npm run build

npm 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 data

  • delete_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 tools
add_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

    B3.4/5.0

    Scored across 23 tools

    Disambiguation4/5

    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.

    Naming Consistency5/5

    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.

    Tool Count4/5

    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.

    Completeness3/5

    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

    ActivityActive
    ResponsivenessNo recent community issues

    Related MCP Connectors

    Related MCP Servers

    • F
      license
      A
      quality
      C
      maintenance
      A 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.
      8
      21
      -
    • A
      license
      A
      quality
      C
      maintenance
      Model 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.
      6
      2,547 npm
      10
      MIT
    • A
      license
      Not graded
      quality
      D
      maintenance
      An MCP server for OData v4 endpoints, especially Microsoft Dataverse/Dynamics 365, enabling authentication, schema discovery, querying, CRUD, and more via natural language.
      MIT