Dataverse MCP Server
The Dataverse MCP Server provides AI agents with structured access to Microsoft Dataverse and Power Platform environments across 14 functional categories:
Environment & Identity: List Power Platform environments, identify the authenticated caller, discover OData EntitySet names, and check user privileges and record-level access rights.
Data Operations: Query records with OData filters or FetchXML (joins, aggregation, paging), retrieve single records by GUID, count and aggregate records, execute bulk
$batchoperations, create/update/delete/upsert records, associate/disassociate records, merge duplicates, and swap connection references in cloud flows.Schema & Metadata: List, inspect, create, update, and delete tables, columns, relationships, choices (option sets), and alternate keys; publish schema customizations.
Solutions & ALM: List, get, create, update, export, import, stage/upgrade, and clone solutions; manage publishers and solution components; track import jobs, view solution history, and analyze dependencies.
Security Administration: List/get security roles, teams, users, and business units; assign/remove roles; audit user access and effective privileges; manage team memberships; enable/disable users.
Audit & History: Query audit records, retrieve before/after details for audit entries, and get full change history for any record.
Plug-in Registration & Diagnostics: List and manage plug-in assemblies, packages, types, steps, and step images; view performance statistics; configure trace log verbosity and query trace logs.
Custom APIs: List, get, create, update, and delete Custom APIs and their request parameters/response properties.
Forms & Views: List, inspect, validate, create, and update model-driven app forms and saved views; add/remove form controls and view columns.
Model-Driven Apps: List, get, validate, create, update, and publish apps; manage app components, sitemaps, and security role assignments.
Cloud Flows & Classic Processes: List, enable/disable cloud flows; list and activate/deactivate classic processes (workflows, business rules, actions, BPFs).
Connection References & Environment Variables: List, get, create, update, and delete connection references, environment variable definitions, and values.
Web Resources: List, get, create, update, delete, and publish web resources (JS, HTML, CSS, images, etc.).
Async Jobs: List, inspect, and cancel system jobs (async operations).
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 list the top 10 active accounts"
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.
An MCP server that gives AI agents structured access to Microsoft Dataverse — query records, bulk upsert data, inspect metadata, manage schema, analyze component dependencies, manage model-driven app forms, views, and apps, administer security roles, teams, and users, audit user access, manage plug-in trace logging, manage custom APIs, and explore Power Platform environments.
Built with MCPServer (mcp.server.mcpserver), httpx, and the Dataverse OData v4.0 Web API. Communicates over stdio and works with Claude, GitHub Copilot, and any MCP-compatible client.
Quick Start
1. Install uv
pip install uv2. Configure — add to your MCP client config:
Claude (claude_desktop_config.json or .claude/settings.json):
{
"mcpServers": {
"dataverse-mcp": {
"command": "uvx",
"args": ["dataverse-mcp"],
"env": {
"DATAVERSE_AUTH_TYPE": "interactive"
}
}
}
}GitHub Copilot (.vscode/mcp.json):
{
"servers": {
"dataverse-mcp": {
"type": "stdio",
"command": "uvx",
"args": ["dataverse-mcp"],
"env": {
"DATAVERSE_AUTH_TYPE": "interactive"
}
}
}
}3. Sign in
On first use the server opens a browser for interactive sign-in. The session is cached and reused across restarts (see DATAVERSE_TOKEN_CACHE_PERSIST), so you are not prompted again while the token is valid.
That's it. Your AI agent can now query your Dataverse environments.
Prefer your existing Azure CLI session instead? Set
DATAVERSE_AUTH_TYPEtoazure_cliand runaz login.
Related MCP server: mcp-dynamics-365
Installation
Install uv
uvx is provided by uv. Install it first if you don't have it:
pip install uvRequires the MCP Python SDK
>=2.0.0. Versions up to and including 3.7.0 require SDK 1.x and will not start against 2.x — the SDK removedmcp.server.fastmcpin 2.0.0, so an older release installed today fails at import withModuleNotFoundError: No module named 'mcp.server.fastmcp'. Becauseuvxignores lockfiles and resolves the latest SDK, use 3.8.0 or later.
Run from PyPI (recommended)
uvx dataverse-mcpuvx downloads and runs the package in an isolated environment — no virtual environment management required.
Run from a local checkout
git clone https://github.com/ryanmichaeljames/dataverse-mcp.git
cd dataverse-mcp
uv syncThis creates .venv. Use the local source MCP config shown in Client Setup to point your client at it. No build step required — code changes are picked up on the next server start.
Configuration
Set these in the env block of your MCP server entry. This project does not use a .env file.
Variable | Default | Description |
|
| Authentication method: |
|
| Set to |
|
| Set to |
| — | Comma-separated list of tool categories to register (e.g., |
| — | Comma-separated list of allowed environment hostnames (e.g., |
|
| When |
| — | Optional directory that confines the solution export/import file paths ( |
|
| Maximum seconds to wait for a credential acquisition (e.g., |
|
| Controls whether |
|
| When |
| — | Optional name that isolates the |
Leaving DATAVERSE_WHITELIST unset is risky. Tools accept a dataverse_url per call, and the server mints a bearer token for whatever environment is supplied. Without a whitelist, a compromised or misbehaving agent can direct your credentials at any Dataverse environment. Set DATAVERSE_WHITELIST to the specific environment hostnames you intend to use so the server rejects everything else. On shared or multi-tenant hosts, also set DATAVERSE_REQUIRE_WHITELIST=true so the server fails closed rather than minting tokens when the whitelist is accidentally left empty.
Authentication
Method | Description |
| Opens a browser for interactive sign-in. Supports MFA and per-account isolation, and needs no separate CLI login. The session persists across server restarts (see |
| Uses your active |
Interactive auth persistence. When DATAVERSE_TOKEN_CACHE_PERSIST=true (the default), the MSAL token cache is stored on disk using your OS secret store (Windows DPAPI, macOS Keychain, Linux libsecret). On headless Linux without libsecret, the first token acquisition will fail fast with an error. Set DATAVERSE_TOKEN_CACHE_ALLOW_UNENCRYPTED=true to permit a plaintext cache on those hosts, and see the security warning for that variable above.
Running multiple tenants/accounts at once. The default cache and sidecar filenames are shared per host, so two interactive sessions signed in to different tenants/accounts would overwrite each other's pinned account. Give each session a distinct DATAVERSE_TOKEN_CACHE_PROFILE (e.g., prod, dev) to keep their caches and AuthenticationRecord sidecars separate.
Example: two tenants side by side
Register two server entries, each with its own DATAVERSE_TOKEN_CACHE_PROFILE. The profile is a tenant-wide cache key — each entry signs in once (its own browser prompt) and then restarts silently as its own account, while tools still receive the specific dataverse_url per call. The profiles never collide.
Claude (claude_desktop_config.json or .claude/settings.json):
{
"mcpServers": {
"dataverse-prod": {
"command": "uvx",
"args": ["dataverse-mcp"],
"env": {
"DATAVERSE_AUTH_TYPE": "interactive",
"DATAVERSE_TOKEN_CACHE_PROFILE": "prod"
}
},
"dataverse-dev": {
"command": "uvx",
"args": ["dataverse-mcp"],
"env": {
"DATAVERSE_AUTH_TYPE": "interactive",
"DATAVERSE_TOKEN_CACHE_PROFILE": "dev"
}
}
}
}GitHub Copilot (.vscode/mcp.json):
{
"servers": {
"dataverse-prod": {
"type": "stdio",
"command": "uvx",
"args": ["dataverse-mcp"],
"env": {
"DATAVERSE_AUTH_TYPE": "interactive",
"DATAVERSE_TOKEN_CACHE_PROFILE": "prod"
}
},
"dataverse-dev": {
"type": "stdio",
"command": "uvx",
"args": ["dataverse-mcp"],
"env": {
"DATAVERSE_AUTH_TYPE": "interactive",
"DATAVERSE_TOKEN_CACHE_PROFILE": "dev"
}
}
}
}Each profile maps to one tenant/account sign-in; agents pass the target dataverse_url on each tool call. Omit DATAVERSE_TOKEN_CACHE_PROFILE (or leave it empty) for a single-tenant setup — the original shared cache filenames are used.
Safety Guards
Most write and delete tools are not registered by default, so they do not appear to the agent until explicitly enabled. One exception is dataverse_execute_batch, which is always visible but only allows GET requests unless DATAVERSE_ALLOW_WRITE=true. This prevents accidental mutations when you only need to read or inspect data while still allowing safe batch reads by default.
{
"servers": {
"dataverse-mcp": {
"type": "stdio",
"command": "uvx",
"args": ["dataverse-mcp"],
"env": {
"DATAVERSE_AUTH_TYPE": "interactive",
"DATAVERSE_ALLOW_WRITE": "true",
"DATAVERSE_ALLOW_DELETE": "true"
}
}
}
}Each flag is independent — set only DATAVERSE_ALLOW_WRITE=true to allow creates and updates while keeping deletes disabled.
Reliability and Limits
These behaviors are built in and need no configuration:
Automatic retries — requests throttled by Dataverse service-protection limits or hitting transient gateway errors are retried automatically; read-only requests are also retried on timeouts and connection failures.
Response size cap — responses larger than 5 MB are replaced with an error asking the agent to narrow the query with
select,top, orfilter.Consistent errors — every tool returns JSON; failures have the shape
{"error": true, "message": "..."}with the Dataverse error code included and the message capped in length.Server-side paging — list tools request right-sized pages from Dataverse instead of full 5,000-record pages.
Client Setup
Claude
Claude Desktop
Add to claude_desktop_config.json:
macOS:
~/Library/Application Support/Claude/claude_desktop_config.jsonWindows:
%APPDATA%\Claude\claude_desktop_config.json
{
"mcpServers": {
"dataverse-mcp": {
"command": "uvx",
"args": ["dataverse-mcp"],
"env": {
"DATAVERSE_AUTH_TYPE": "interactive"
}
}
}
}Claude Code
Add via the CLI:
claude mcp add dataverse-mcp --env DATAVERSE_AUTH_TYPE=interactive uvx dataverse-mcpOr add directly to .claude/settings.json (project) or ~/.claude/settings.json (user):
{
"mcpServers": {
"dataverse-mcp": {
"command": "uvx",
"args": ["dataverse-mcp"],
"env": {
"DATAVERSE_AUTH_TYPE": "interactive"
}
}
}
}Run from a local checkout
{
"mcpServers": {
"dataverse-mcp-local": {
"command": "C:\\path\\to\\dataverse-mcp\\.venv\\Scripts\\python.exe",
"args": ["-m", "dataverse_mcp.server"],
"env": {
"PYTHONPATH": "C:\\path\\to\\dataverse-mcp\\src",
"DATAVERSE_AUTH_TYPE": "interactive"
}
}
}
}GitHub Copilot
Add to .vscode/mcp.json in your project root.
Run from PyPI
{
"servers": {
"dataverse-mcp": {
"type": "stdio",
"command": "uvx",
"args": ["dataverse-mcp"],
"env": {
"DATAVERSE_AUTH_TYPE": "interactive"
}
}
}
}Run from a local checkout
{
"servers": {
"dataverse-mcp-local": {
"type": "stdio",
"command": "C:\\path\\to\\dataverse-mcp\\.venv\\Scripts\\python.exe",
"args": ["-m", "dataverse_mcp.server"],
"env": {
"PYTHONPATH": "C:\\path\\to\\dataverse-mcp\\src",
"DATAVERSE_AUTH_TYPE": "interactive"
}
}
}
}Multi-environment targeting
A single server instance can target any Dataverse org — pass dataverse_url on each tool call:
{
"dataverse_url": "https://yourorg.crm.dynamics.com",
"entity_set_name": "accounts",
"select": ["name", "accountid"],
"top": 10
}dataverse_url is required on every tool call. Use dataverse_list_environments to discover available environments if you do not yet know the URL.
Tools
200 tools grouped by domain below. Every tool returns JSON and requires dataverse_url on each call.
The Gate column shows when a tool is registered:
Gate | Meaning |
| Always registered (reads and safe queries). |
| Registered only when |
| Registered only when |
dataverse_execute_batchisdefaultbut rejects non-GET operations unlessDATAVERSE_ALLOW_WRITE=true.
Tool categories
Use DATAVERSE_TOOLS to register only the tool categories your agent needs. This shrinks the visible tool list and reduces token overhead.
Category | Tools | Description |
| 24 | Environment introspection, effective org settings, all record CRUD, and unpublished-customization reads (always registered) |
| 35 | Table/column/relationship/choice/alternate-key metadata, component customizability pre-flight, environment language codes |
| 21 | Solution and publisher management, solution components, history, import/export ALM, import diagnostics, dependency analysis |
| 8 | Cloud flow + classic process listing and activate/deactivate |
| 6 | Model-driven form management |
| 7 | Saved query / view management |
| 10 | Canvas and model-driven app management |
| 5 | Connection reference management |
| 8 | Environment variable definitions and values |
| 33 | Plugin assemblies, types, steps, step images, packages, trace logs |
| 22 | Security roles and their privileges, teams and their privileges, the environment-wide privilege catalogue, users, business units, record access origin, record shares, composite access audit, record- and column-level audit history |
| 3 | Async operation (system job) monitoring and cancellation |
| 5 | Web resource (JS/HTML/CSS/image) CRUD — gated, not always-on |
| 13 | Custom API, request parameter, and response property management |
core is always registered even when not listed. When DATAVERSE_TOOLS is unset or empty, all categories register (current default behaviour). Category gating composes with DATAVERSE_ALLOW_WRITE and DATAVERSE_ALLOW_DELETE: a tool registers only when its category is enabled AND its write/delete flag (if any) is set.
Environment & identity
Tool | Gate | Description |
| default | List Power Platform environments accessible to the caller |
| default | Return the caller's |
| default | Fingerprint the environment — server version, organization identity, instance type, service endpoints, installed-solution count |
| default | Read one setting's final computed value via |
| default | List OData EntitySet names from the service document |
| default | List security privileges assigned to a user |
| default | Check a user's access rights to a specific record |
Security administration
Tool | Gate | Description |
| default | List security roles, optional filter and pagination |
| default | Get one security role by GUID |
| default | List the privileges assigned to a security role via |
| default | Answer why a principal has access to one record — role, ownership, share, business-unit hierarchy, team membership — via |
| default | List teams, optional filter and pagination |
| default | Get one team by GUID |
| default | Answer "what can this team actually do?" via the entity-bound |
| default | List the privileges defined in the environment from the |
| default | List everyone one record was shared with, merging |
| default | List system users, optional filter and pagination |
| default | Get one system user by GUID |
| default | List business units, optional filter and pagination |
| default | Composite report: user identity, direct roles, team memberships + team roles, effective privileges, optional record-level access check |
| default | Query the |
| default | Get full before/after detail for a single audit record via the bound |
| default | Retrieve the full audit change history for a single record via |
| default | Audit trail for one column of one record via |
| write | Assign a security role to a user or team |
| write | Add one or more users to a team |
| write | Enable or disable a system user ( |
| delete | Remove a security role from a user or team |
| delete | Remove one or more users from a team |
Async jobs
Tool | Gate | Description |
| default | List system jobs (asyncoperations), optional filter by statecode/statuscode/operationtype |
| default | Get one system job by GUID |
| write | Cancel a running or waiting system job (PATCH statecode=3/statuscode=32) |
Web resources
Tool | Gate | Description |
| default | List web resources; optional filter by type and/or name substring |
| default | Get one web resource by GUID; |
| write | Create a web resource (name, type, base64 content); call |
| write | PATCH content, display name, or description; call |
| delete | Permanently delete an unmanaged web resource by GUID |
Records & data
Tool | Gate | Description |
| default | Query records with filter, select, orderby, expand, top |
| default | Execute a FetchXML query (joins, aggregation, paging cookie) |
| default | Pre-flight a FetchXML query via |
| default | Get one record by entity set name and GUID |
| default | Read the unpublished (draft) definition of one customization record via |
| default | Count rows in a table, optional filter |
| default | Approximate row counts for up to 50 tables in one call ( |
| default | Aggregate (sum, avg, min, max, countdistinct) with optional grouping |
| default | Run up to 1,000 OData operations in one |
| write | Upsert many records via |
| write | Create a record and return its new GUID |
| write | Partially update a record (PATCH) |
| write | Swap a connection reference logical name inside a cloud flow's |
| write | Associate two records via a collection-valued navigation property |
| write | Merge a subordinate record into a target (account, contact, lead, incident) |
| delete | Permanently delete a record |
| delete | Remove an association between two records |
Tables & columns
Tool | Gate | Description |
| default | List tables, optional metadata filter |
| default | Get full schema details for a table |
| default | List columns for a table, optional type filter |
| default | Get full metadata for one column, including type-specific properties |
| default | Report the environment's language codes (LCIDs), reconciling |
| default | Pre-flight check via |
| write | Create a custom table (ownership type, primary name attribute) |
| write | Update a table's display name or description |
| write | Add a typed column to a table (supports Memo, Boolean with custom labels, and Picklist/MultiSelectPicklist bound to a global choice) |
| write | Replace a column via full PUT (fetch with |
| write | Publish schema changes via |
| delete | Permanently delete a custom table and all its data |
| delete | Permanently delete a custom column and all its data |
Relationships
Tool | Gate | Description |
| default | List relationships for a table or the whole environment |
| default | Get full metadata for one relationship by schema name |
| default | Answer "is this table eligible?" — a per-table boolean for one table you can already name; use |
| default | Answer "which tables are eligible?" — enumerate the tables that may take a relationship role via |
| write | Create a 1:N relationship and its lookup column |
| write | Create an N:N relationship and its intersect table |
| write | Create a polymorphic lookup referencing multiple tables |
| write | Replace a relationship via full PUT (fetch with |
| delete | Delete a custom relationship by MetadataId |
Choices (option sets)
Tool | Gate | Description |
| default | List global choices (option sets) |
| default | Get a global choice and its options by name or MetadataId |
| default | Get options for a Picklist or MultiSelectPicklist column |
| write | Create a global choice with initial options |
| write | Replace a global choice via full PUT (fetch with |
| write | Add an option to a global or local choice |
| write | Update the display label of an option |
| write | Reorder all options in a choice |
| delete | Delete a global choice by logical name |
| delete | Remove one option from a global or local choice |
Alternate keys
Tool | Gate | Description |
| default | List |
| write | Create an alternate key by |
| delete | Remove an alternate key by logical name; drops the underlying SQL index |
Solutions & publishers
Tool | Gate | Description |
| default | List solutions, optional filter and pagination |
| default | Get a solution by unique name or GUID |
| default | List components in a solution, optional type filter |
| default | Get one solution history record (import/upgrade/export operation) by GUID |
| default | List solution history records, optional filter by solution GUID or unique name; |
| write | Create a publisher with customization prefixes |
| write | Update publisher fields by GUID |
| write | Create a solution (publisher binding, version) |
| write | Update solution fields by GUID or unique name |
| write | Update only a solution's version |
| write | Add a component via |
| delete | Remove a component via |
| default | Export a solution as a base64 zip; write to disk via |
| write | Import a solution asynchronously via |
| write | Single-step solution upgrade via |
| write | Two-step apply-upgrade via |
| default | Get one importjob by GUID — returns progress, completedon, solutionname; add |
| default | Why did the import fail? — the platform's own human-readable results document for one importjob via |
| default | List importjobs, optional filter by solution unique name, ordered by createdon desc |
| write | Clone a solution as a patch via bound |
| default | Analyze component dependencies: |
Filesystem I/O note.
dataverse_export_solutioncan write the decoded .zip to a local path whenoutput_pathis supplied.dataverse_import_solutioncan read a local .zip wheninput_pathis supplied. Both paths are resolved on the machine running the MCP server. Useoutput_path/input_pathfor solutions larger than ~3 MB (the inline base64 threshold).
Cloud flows & processes
Tool | Gate | Description |
| default | Query cloud flows, optionally scoped to a solution |
| write | Enable one flow by workflow ID |
| write | Enable many flows in one |
| write | Disable one flow by workflow ID |
| write | Disable many flows in one |
| default | List classic processes (workflows, business rules, actions, BPFs) from the |
| write | Activate a classic process (sets statecode=1/Activated); idempotent |
| write | Deactivate a classic process (sets statecode=0/Draft); idempotent |
Forms
Tool | Gate | Description |
| default | List forms for a table, optional form type filter |
| default | Get a form's layout as a tabs → sections → controls tree |
| default | Validate FormXml against XSD; pass |
| write | Replace and publish a form's FormXml; returns |
| write | Add a column control to a form (auto-resolves classid) |
| write | Remove a column control by logical name |
dataverse_get_formreturns the published form. After any write above, read the draft back withdataverse_retrieve_unpublished(entity_set_name='systemforms') or publish first withdataverse_publish_customizations.
Views
Tool | Gate | Description |
| default | List saved views (savedquery) for a table, optional query type filter |
| default | Get a view's FetchXml, LayoutXml, and column list |
| default | Validate a view's FetchXml and LayoutXml |
| write | Create a saved view with FetchXml and LayoutXml |
| write | Update a view's FetchXml, LayoutXml, name, or description |
| write | Add a column to a view's LayoutXml |
| write | Remove a column from a view's LayoutXml |
dataverse_get_viewreturns the published view. After any write above, read the draft back withdataverse_retrieve_unpublished(entity_set_name='savedqueries') or publish first withdataverse_publish_customizations.
Model-driven apps
Tool | Gate | Description |
| default | List apps; |
| default | Get an app's properties and components grouped by type |
| default | Validate an app via |
| write | Create an app (sitemap, components, validation, publish) |
| write | Update an app's name or description |
| write | Add tables, forms, views, charts, or BPFs to an app |
| write | Remove components from an app |
| write | Create or replace an app's navigation sitemap |
| write | Publish an app to make it visible to users |
| write | Associate or disassociate a security role with an app |
Connection references
Tool | Gate | Description |
| default | List connection references, optional connector/status/OData filters |
| default | Get one by GUID or logical name |
| write | Create one, optional connection and solution association |
| write | Assign/clear connection, update fields, or associate with a solution |
| delete | Delete an unmanaged connection reference |
Environment variables — definitions
Tool | Gate | Description |
| default | List definitions with current values; optional solution filter or single-record name lookup (schema name / display name) |
| write | Create a definition and optional initial value |
| write | Update definition fields and/or upsert the current value |
| delete | Delete definition, value record, or both |
Environment variables — values
Tool | Gate | Description |
| default | Get value record(s) by value GUID, definition GUID, or definition name |
| write | Create a value record bound to a definition (by GUID or name) |
| write | PATCH an existing value record by value GUID, definition GUID, or definition name |
| delete | Delete a value record only (resets to default value) by value GUID, definition GUID, or definition name |
Plug-in registration
Tool | Gate | Description |
| default | List registered plug-in assemblies |
| default | Get one plug-in assembly |
| default | List NuGet plug-in packages |
| default | Get one plug-in package |
| default | List plug-in types (classes) in an assembly |
| default | Get one plug-in type |
| default | List SDK message processing step registrations |
| default | Get one processing step |
| default | List pre/post entity images on a step |
| default | Get one step image |
| default | List SDK messages (Create, Update, …) — reference |
| default | Get one SDK message |
| default | List SDK message filters (message/entity combos) — reference |
| default | Get one SDK message filter |
| write | Register a plug-in assembly |
| write | Update a plug-in assembly |
| write | Register a plug-in package |
| write | Update a plug-in package |
| write | Register a plug-in type |
| write | Update a plug-in type |
| write | Register an SDK message processing step |
| write | Update a processing step |
| write | Register a step image |
| write | Update a step image |
| delete | Delete a plug-in assembly |
| delete | Delete a plug-in package |
| delete | Delete a plug-in type |
| delete | Delete a processing step |
| delete | Delete a step image |
Plug-in tracing & statistics
Tool | Gate | Description |
| default | Runtime performance stats (execution count, failure rate, crashes) per plug-in type |
| default | Get org-wide trace log verbosity (off / exception / all) |
| default | List trace logs with filters (class, message, entity, operation, errors-only, time window) |
| write | Set org-wide trace log verbosity ( |
Custom APIs
Tool | Gate | Description |
| default | List custom APIs, optional filter and pagination |
| default | Get one custom API by GUID, including its request parameters and response properties |
| write | Create a custom API (unbound, entity-bound, or entity collection-bound) |
| write | Update mutable fields of a custom API (display name, description, visibility, allowed step types) |
| delete | Permanently delete a custom API and its child parameters and properties |
| default | List request parameters for a custom API |
| write | Add a typed request parameter to a custom API |
| write | Update mutable fields of a request parameter |
| delete | Delete a request parameter |
| default | List response properties for a custom API |
| write | Add a typed response property to a custom API |
| write | Update mutable fields of a response property |
| delete | Delete a response property |
Development
# Install dependencies
uv sync
# Run the MCP inspector (interactive testing)
uv run mcp dev src/dataverse_mcp/server.py
# Run the server directly
uv run python -m dataverse_mcp.server
# Compile check
uv run python -m py_compile src/dataverse_mcp/server.pyRestart the MCP server in VS Code after code changes to pick up the new source.
License
MIT
Disclaimer
Independent community project. Not affiliated with, endorsed by, or supported by Microsoft. For the first-party runtime server, see Microsoft's Dataverse MCP Server.
"Dataverse" is a trademark of the President and Fellows of Harvard College. "Microsoft Dataverse" and "Power Platform" are trademarks of the Microsoft group of companies. Used here only to describe the systems this tool works with.
Available Tools
97 toolsdataverse_aggregate_tableARead-onlyIdempotent
Group and aggregate Dataverse records with an OData $apply expression.
Use this for per-group questions (e.g. count by status, sum revenue by region). For a single total count use dataverse_count_records; for raw rows use dataverse_query_table. Works on up to 50,000 records. See the apply parameter for expression examples.
Inputs: params (required). Returns: result
dataverse_analyze_dependenciesARead-onlyIdempotent
Analyze dependencies for a Dataverse solution component.
Exposes three directions via the direction parameter:
blocking_delete: components that must be removed before this one can be deleted.
dependents: all components that reference/depend on this component.
required: all components this component requires to exist.
Use component_type integer codes (1=Entity, 2=Attribute, 61=WebResource, etc.) and the component's metadata GUID for component_id.
Inputs: params (required). Returns: result
dataverse_audit_user_accessARead-onlyIdempotent
Return a composite access report for a Dataverse system user.
Gathers in one call: user identity, direct security roles, team memberships (with each team's roles), and optionally effective privileges and record-level access rights. Resolves all type codes to human-readable names.
Provide either user_id (GUID) or user_domain_name (e.g. 'user@contoso.com'). Optionally provide target_entity_set_name + target_record_id to include a record-level access check.
Inputs: params (required). Returns: result
dataverse_check_relationship_eligibilityARead-onlyIdempotent
Pre-validate whether a table supports a specific relationship role before creating a relationship.
Only call this immediately before dataverse_create_one_to_many_relationship or dataverse_create_many_to_many_relationship — do not use for general queries or data reads. Returns eligible (bool) for the requested check_type (see check_type field for valid values).
This tool answers "is THIS ONE table OK?" — a boolean about a table you can already name. To answer "WHICH tables are OK?" — the enumeration, when you do not yet know which table to point at — use dataverse_get_valid_relationship_entities, whose role values mirror this tool's check_type values one for one.
Inputs: params (required). Returns: result
dataverse_count_recordsARead-onlyIdempotent
Count records in a table (optionally filtered) and return only the integer total.
Use this instead of dataverse_query_table when you need a number, not rows. For per-group counts (e.g. count by status) use dataverse_aggregate_table. The total is capped at 5,000 by Dataverse.
Inputs: params (required). Returns: result
dataverse_execute_batchA
Execute bulk or atomic multi-operation reads and writes via the OData $batch endpoint.
Use this for bulk record operations or when multiple writes must succeed or fail together. For single-record writes use dataverse_create_record / dataverse_update_record / dataverse_delete_record instead. For metadata/schema changes use the dataverse_create_*/update_*/delete_* metadata tools.
POST/PUT/PATCH operations require DATAVERSE_ALLOW_WRITE=true; DELETE operations require DATAVERSE_ALLOW_DELETE=true. Group operations with the same change_set_id to run them atomically (all-or-nothing, up to 1,000 operations per request). Returns per-operation results [{index, status_code, body}].
Inputs: params (required). Returns: result
dataverse_execute_fetchxmlARead-onlyIdempotent
Execute a FetchXML query against a Dataverse table and return matching records.
FetchXML supports complex joins (link-entity), aggregation, and queries that OData $filter cannot express. Use dataverse_query_table for simple OData queries. Use dataverse_get_entity_sets to discover entity_set_name; the entity_set_name must match the root logical name's collection name.
FetchXML uses paging cookies (not @odata.nextLink). This tool returns one page plus paging metadata (has_more, paging_cookie) so the caller can page if needed.
Inputs: params (required). Returns: result
dataverse_export_solutionARead-onlyIdempotent
Export a Dataverse solution as a base64-encoded zip.
Calls the ExportSolution unbound action. Large solutions (>~3 MB base64) must be saved to disk via output_path — supply a local filesystem path and the server writes the decoded .zip there, returning metadata only (no base64 in the response). Small solutions are returned inline when output_path is omitted. This tool is read-only for the org (no mutations); writing a local .zip when output_path is set is local I/O only and does not require DATAVERSE_ALLOW_WRITE.
Inputs: params (required). Returns: result
dataverse_get_appARead-onlyIdempotent
Get a model-driven app's properties and its current component list.
Returns app metadata and components grouped by type (Entity, View, Form, Sitemap, etc.) via RetrieveAppComponents. Use dataverse_list_apps to find app IDs. Call this before any write to confirm current component state.
Inputs: params (required). Returns: result
dataverse_get_async_operationARead-onlyIdempotent
Retrieve a single asyncoperation (system job) record by its GUID.
Returns the full record including name, operationtype, statecode, statuscode, message, friendlymessage, startedon, completedon, and _regardingobjectid_value. Also includes statecode_label and statuscode_label for human readability.
Inputs: params (required). Returns: result
dataverse_get_attribute_change_historyARead-onlyIdempotent
Retrieve the audit trail for ONE COLUMN of ONE RECORD — who changed this field.
The column-scoped sibling of dataverse_retrieve_record_change_history, which returns every change to the record across all audited columns. Use this one when the question is about a single field ('when did this account's creditlimit last change, and to what?'); it answers from the server rather than making you filter a whole record's history client-side.
MIND THE SINGULAR/PLURAL SPLIT — this tool takes the SAME TABLE TWICE, under two different names, and they are NOT interchangeable:
entity_set_name — the PLURAL collection name ('accounts'). This is the only one sent to the function, inside the target EntityReference.
table_logical_name — the SINGULAR logical name ('account'). Never sent to the function; used only by the audit-configuration probes described below, which address table metadata by LogicalName. Both are required because the plural cannot be derived from the singular (or vice versa: 'webresource' -> 'webresourceset'), and it cannot be looked up either — $filter on the root EntityDefinitions collection is refused with HTTP 400 [0x80060888]. Use dataverse_get_entity_sets to confirm the plural.
ON A 404, READ THE ERROR CODE — DO NOT ASSUME WHICH FAILURE IT IS. Live-confirmed on this function, and the older warning that a missing record and a wrong entity set were indistinguishable was wrong:
a well-formed but NONEXISTENT record id with the CORRECT plural entity set returns HTTP 200, not a 404. The function did not check that the record exists, so a bogus GUID (or a deleted record) simply yields zero changes and the audit_configuration diagnosis below. A successful empty answer is therefore never proof that the record is there.
a VALID id with the WRONG (singular) entity set returns HTTP 404 [0x80060888] "Resource not found for the segment ''" — which NAMES the bad segment. That message is the singular-for-plural slip, not a missing record: fix the entity set name rather than hunting a deleted row. Do NOT read those two as an exhaustive split. On the sibling function RetrieveRecordChangeHistory, a sweep of 15 entity sets found a THIRD outcome — [0x80048d02] from a CORRECT plural entity set ('audits'), meaning the row really was absent, i.e. some entity sets DO validate the target. That sweep was run against the record-scoped function, not this one, so it is not confirmed here; it is reason enough to read the code that actually came back rather than trust a two-case rule. THE ALL-ZERO GUID IS HANDLED DIFFERENTLY BY THE TWO FUNCTIONS, live-confirmed. Passing 00000000-0000-0000-0000-000000000000 as record_id returns HTTP 200 with zero changes HERE, but the record-scoped dataverse_retrieve_record_change_history rejects the same id with HTTP 400 [0x80040203] "Expected non-empty Guid." That is Dataverse's own inconsistency between the two messages, not this server's. So an empty, successful answer from this tool can mean the caller passed a placeholder id — check the id before reading zero changes as a fact about the record.
AN EMPTY RESULT IS AMBIGUOUS, AND THIS TOOL RESOLVES IT. Audit rows are written only where auditing is enabled at organization AND table AND column level, so zero changes cannot by itself distinguish "nothing ever changed" from "auditing was never switched on". ONLY when there are zero changes, three probes fire concurrently and an audit_configuration block is attached carrying organization_audit_enabled / table_audit_enabled / column_audit_enabled and a diagnosis naming the OUTERMOST disabled level:
auditing_off_at_organization / auditing_off_at_table / auditing_off_at_column
auditing_enabled_no_changes_recorded — all three on, so the empty answer is real (auditing still only records changes made after it was switched on)
undetermined — a probe failed or returned an unreadable shape; the level is reported as null with the reason in probe_errors, and NOTHING is guessed.
NOT EVERY ENTRY IS A RESULT. Dataverse MAY add org-level audit-CONFIGURATION rows (auditing itself switched on or off) to a response. They arrive when an audit-configuration change falls inside the TARGET RECORD'S history window, so their presence and count VARY BY TARGET — a record created after the last such change gets none, while older records on the same org got four each, live-measured. audit_configuration_events_count: 0 is a normal, expected answer. They can arrive anywhere in the list and are identified by their SHAPE — no @odata.type, AuditRecord and nothing else, and an all-zero AuditRecord._objectid_value — never by their position. They are split out into audit_configuration_events (with audit_configuration_events_count) and are NOT counted as changes: audit_details, count and has_more cover this column's own changes only.
ENTRIES ARE POLYMORPHIC. Each change is returned verbatim, so read its @odata.type: a column-scoped call is expected to yield AttributeAuditDetail (AuditRecord, OldValue, NewValue, InvalidNewValueAttributes, plus AuditRecord.attributemask naming the changed columns) but nothing guarantees it — AuditDetail has several subtypes. An entry with an UNRECOGNIZED @odata.type is reported as a change, never quietly dropped; only the typeless AuditRecord-only shape with an all-zero objectid is treated as configuration. detail_types counts the @odata.type values actually present on the returned page, and unclassified_typeless_count reports how many entries carrying NO @odata.type were kept as changes — 0 on every response observed so far, and a non-zero value means this tool met an entry it could not name rather than that anything was lost.
RESPONSE SHAPE IS CHECKED, NOT ASSUMED. The entries sit TWO levels down (AuditDetailCollection -> AuditDetails). If that container is absent or is not a list, the tool returns normalized: false with the raw body — a missing container is NOT reported as "no changes".
PagingInfo is not sent, so the changes are trimmed client-side to top and has_more reports whether anything was cut. total_record_count appears ONLY when Dataverse supplied a real count: it is live-confirmed to arrive as -1 here both with and without PagingInfo, and a negative count is suppressed rather than passed on. It is never substituted with the number of returned entries.
URL form: GET /api/data/v9.2/RetrieveAttributeChangeHistory( Target=@t,AttributeLogicalName=@a) ?@t={"@odata.id":"accounts()"}&@a='name'
Inputs: params (required). Returns: result
dataverse_get_audit_detailsARead-onlyIdempotent
Retrieve full details from a single audit record.
Calls the bound RetrieveAuditDetails function on the audit entity, returning a polymorphic AuditDetail. The most common subtype is AttributeAuditDetail which includes OldValue and NewValue (each containing the changed attribute values keyed by logical name) plus InvalidNewValueAttributes.
Common AuditDetail subtypes (identified by @odata.type):
AttributeAuditDetail — field changes with OldValue/NewValue
RelationshipAuditDetail — relationship association/disassociation
ShareAuditDetail — record sharing
RolePrivilegeAuditDetail — role privilege changes
UserAccessAuditDetail — user login/access events
Note: requires auditing enabled on the org. If auditing is disabled, Dataverse returns an HTTP error — check the error message for guidance.
URL form: GET /api/data/v9.2/audits(<audit_id>)/Microsoft.Dynamics.CRM.RetrieveAuditDetails
Inputs: params (required). Returns: result
dataverse_get_choiceARead-onlyIdempotent
Get one GLOBAL choice (option set) — all option values, codes, and labels — by name or MetadataId.
For the options of a specific column's LOCAL choice use dataverse_list_choice_column_options instead. Provide either name or metadata_id; name takes precedence when both are given.
Inputs: params (required). Returns: result
dataverse_get_cloud_flowsARead-onlyIdempotent
List cloud flows in the Dataverse environment, optionally scoped to a solution.
Returns workflow records with statecode, statuscode, and category. Scope to a specific solution with solution_id or solution_unique_name.
Inputs: params (required). Returns: result
dataverse_get_columnARead-onlyIdempotent
Get full metadata for a single column on a Dataverse table, including type-specific properties.
Returns all properties including MaxLength, Precision, RequiredLevel, Format, and IsValidForCreate. Use before updating a column — pass the returned object as full_definition to dataverse_update_column. For Picklist/MultiSelectPicklist option values use dataverse_list_choice_column_options.
Inputs: params (required). Returns: result
dataverse_get_connection_referenceARead-onlyIdempotent
Get a single connection reference by GUID or logical name.
Returns the full record including the assigned connection ID, connector ID, status, and managed state. An empty connectionid means no connection has been wired up yet — use dataverse_update_connection_reference to assign one.
Inputs: params (required). Returns: result
dataverse_get_custom_apiARead-onlyIdempotent
Retrieve a single Custom API record by its GUID, including expanded request parameters and response properties.
Returns all default columns plus:
CustomAPIRequestParameters (customapirequestparameterid, uniquename, name, type, isoptional)
CustomAPIResponseProperties (customapiresponsepropertyid, uniquename, name, type) The @odata.context key is stripped from the response.
Inputs: params (required). Returns: result
dataverse_get_entity_setsARead-onlyIdempotent
List OData EntitySet names from the Dataverse service document.
Use this to discover the correct entity_set_name for a table before querying records (e.g., 'account' → 'accounts', 'systemuser' → 'systemusers'). Faster and smaller than fetching $metadata. Filter with contains.
Inputs: params (required). Returns: result
dataverse_get_environment_variablesARead-onlyIdempotent
List environment variable definitions with their current values.
Each record includes the definition fields (schemaname, displayname, type, defaultvalue, description, ismanaged) plus the current value from the linked environmentvariablevalue record. The value field is null when no value record exists — fall back to defaultvalue in that case.
Provide name (schemaname or displayname) to look up a single definition. Schema name is tried first; display name is the fallback. name cannot be combined with solution_id or solution_unique_name.
Scope results to a specific solution with solution_id or solution_unique_name (componenttype 380 solutioncomponents query). Omit both to list all definitions in the environment.
Inputs: params (required). Returns: result
dataverse_get_environment_variable_valuesARead-onlyIdempotent
Get environment variable value record(s).
Provide exactly one targeting path:
value_id: fetch a single value record by its own GUID directly.
definition_id: list value record(s) linked to the given definition GUID.
name: resolve the definition by schema name or display name, then list its value record(s). Schema name is tried first; display name is the fallback.
Returns a list shape (records, count, has_more) in all cases for a consistent calling convention, even when a single record is expected. When no value record exists for a definition the list will be empty.
Inputs: params (required). Returns: result
dataverse_get_formARead-onlyIdempotent
Get a single Dataverse form's layout parsed as a structured JSON object.
Parses raw FormXml into a readable tabs → sections → controls tree so agents don't need to work with raw XML. Also returns formxml_backup (the raw XML string). Use dataverse_list_forms to discover form IDs.
Inputs: params (required). Returns: result
dataverse_get_import_jobARead-onlyIdempotent
Retrieve a single importjob record by its GUID to check import progress.
Returns progress (0–100), completedon, solutionname, and other tracking fields. The large result XML ('data' column) is excluded by default; set include_data=true to fetch it when diagnosing failures. Use dataverse_import_solution or dataverse_stage_and_upgrade_solution to start an operation and obtain the import_job_id.
When include_data=true, the 'data' result XML contains component-level detail for all import phases, including the DeleteComponents phase of an upgrade. Error code 0x8004F037 (image-column dependency failures) and other component-level errors from the deletion phase appear in this XML.
Inputs: params (required). Returns: result
dataverse_get_import_job_resultsARead-onlyIdempotent
Answer "WHY did this solution import fail?" — the readable import results.
Calls the unbound RetrieveFormattedImportJobResults function, which returns the platform's own human-readable results document for one import job.
This is the companion to dataverse_get_import_job, which returns the importjob RECORD (progress, completedon, solutionname) and, with include_data=true, the raw 'data' column — a large opaque XML blob you then have to parse yourself to find the failure. This tool asks Dataverse to format those results instead. Use dataverse_list_import_jobs (most recent first) or the import_job_id returned by dataverse_import_solution / dataverse_stage_and_upgrade_solution to get an id.
RESPONSE SHAPE — LIVE-VERIFIED. The document arrives as ONE string property named FormattedResults, and the body carries nothing else. It is surfaced as 'results', with results_source naming the property it was read from. Nothing in the document is interpreted: the text is passed through verbatim. Microsoft Learn documents the function and its return type but NOT that type's inner properties, so the property is still located by name and then by shape rather than assumed; if the payload cannot be identified unambiguously, normalized is false, no length or summary is reported, and the payload comes back unchanged (minus the @odata.* envelope) under raw_response — read it yourself rather than trusting a guess.
WHAT YOU GET BACK IS A SPREADSHEET. Live-verified: 'results' is a SpreadsheetML (Excel XML) workbook — root element Workbook, an mso-application progid="Excel.Sheet" processing instruction and the urn:schemas-microsoft-com:office:spreadsheet namespace — NOT a Dataverse results schema. Its element names are spreadsheet furniture (Worksheet, Table, Row, Cell, Data, Style, Font, Interior, Border...) and NONE of them is named error, warning or failure. The meaning lives in the CELL VALUES, so to find out why an import failed you must read the TEXT of the document, not its tags. The same format came back for a completed job and a still-running one.
THE DOCUMENT IS TRIMMED BY DEFAULT, AND IT IS BIG. The function has no server-side paging — it returns the whole document in one string, and observed documents ran to tens of thousands of characters (about 14,000 for a small import, about 71,000 for a larger one), so the default WILL usually truncate. The first max_chars characters (default 20,000) are returned inline and the true size is never hidden: results_length is ALWAYS the full character count Dataverse returned and truncated says whether anything was cut. Raise max_chars (max 2,000,000) to read more; an import failure's reason is usually near the top, but spreadsheet markup is verbose, so budget generously.
A STRUCTURAL SUMMARY, NOT A VERDICT. When the document parses as XML, the summary reports root_tag, element_count, distinct_tag_count and element_counts (a per-tag tally) — computed over the WHOLE document, not just the returned slice, so it describes what you did not see as well as what you did. It is deliberately descriptive only: no node is labelled an error, a warning or a failure, because the document's schema is undocumented and a wrong "the import succeeded" verdict is worse than none. Given the SpreadsheetML format above, the tally counts spreadsheet structure and tells you little about the import itself — element_counts_note repeats that warning in the response. Read the text. If the document does not parse (it may be HTML or plain text), markup_parsed is false with a note explaining why and the text is still returned in full. Parsing uses a hardened parser that refuses XML entity declarations and external entity references outright.
Note the two ids are not interchangeable: this takes the importjob GUID (ImportJobId / importjobid), not the separate ImportJobKey string that ImportSolutionAsync also returns.
Inputs: params (required). Returns: result
dataverse_get_organization_infoARead-onlyIdempotent
Fingerprint a Dataverse environment: server version, organization identity, endpoints.
Merges three unbound Web API functions — RetrieveVersion, RetrieveCurrentOrganization, and RetrieveOrganizationInfo. Call this before any risky operation to confirm which environment you are pointed at.
To tell a non-production environment from production, read organization_info.organizationInfo.InstanceType or current_organization.Detail.OrganizationType. Both are strings, and their values are distinct per tier — a developer-tier org reports "Developer", not "Sandbox" — so never test only for "Sandbox" when deciding whether an environment is safe to change. Identity lives alongside them: Detail.UniqueName, Detail.FriendlyName, Detail.EnvironmentId, Detail.Geo, and Detail.State.
RetrieveOrganizationInfo also returns every installed solution. That list runs to several hundred entries, so it is replaced by organization_info.organizationInfo.solutions_count. Set include_solutions=true to get the full Solutions array as well, but prefer dataverse_list_solutions for browsing solutions.
Each function is called independently. If one is unavailable or privilege-gated its failure is reported in partial_errors and the remaining data is still returned; only a failure of all three yields an error response. Apart from the solution summarization, payloads are returned as Dataverse produced them, minus the @odata envelope keys.
Inputs: params (required). Returns: result
dataverse_get_plugin_assemblyARead-onlyIdempotent
Retrieve a single plug-in assembly record by its GUID.
The 'content' column contains the base64-encoded DLL and is very large — exclude it from select unless you specifically need it.
Inputs: params (required). Returns: result
dataverse_get_plugin_packageARead-onlyIdempotent
Retrieve a single plug-in package record by its GUID.
Packages are an alternative to raw assemblies — Dataverse extracts the contained assemblies automatically on upload.
Inputs: params (required). Returns: result
dataverse_get_plugin_stepARead-onlyIdempotent
Retrieve a single SDK message processing step record by its GUID.
Steps are the third node in the prerequisite chain: assembly → type → step → image.
Inputs: params (required). Returns: result
dataverse_get_plugin_step_imageARead-onlyIdempotent
Retrieve a single plug-in step image record by its GUID.
Step images are pre/post entity snapshots passed to the plug-in context. They are the leaf node in the chain: assembly → type → step → image.
Inputs: params (required). Returns: result
dataverse_get_plugin_trace_log_settingARead-onlyIdempotent
Get the current plug-in trace log setting for the Dataverse organization.
Returns the verbosity level: off (0), exception (1), or all (2).
This setting controls whether Dataverse records plug-in execution traces in the plugintracelog entity. Use dataverse_set_plugin_trace_log_setting to change the setting, and dataverse_list_plugin_trace_logs to read the logs.
Inputs: params (required). Returns: result
dataverse_get_plugin_typeARead-onlyIdempotent
Retrieve a single plug-in type record by its GUID.
Plug-in types represent individual .NET classes within an assembly. Use dataverse_list_plugin_types to browse types in a given assembly.
Inputs: params (required). Returns: result
dataverse_get_recordARead-onlyIdempotent
Retrieve a single Dataverse record by its GUID.
For multiple records with filtering use dataverse_query_table. Use dataverse_query_table first to find record IDs if you do not have one.
Inputs: params (required). Returns: result
dataverse_get_relationshipARead-onlyIdempotent
Get full metadata for a single relationship by schema name — cascade configuration and navigation properties.
Schema names are case-sensitive; use the exact SchemaName from dataverse_list_relationships. Use this to fetch the full definition before updating with dataverse_update_relationship.
Inputs: params (required). Returns: result
dataverse_get_role_privilegesARead-onlyIdempotent
Answer "what can this security role actually DO?" — list a role's privileges.
Calls the unbound RetrieveRolePrivilegesRole function. This is the companion to dataverse_get_security_role, which returns the role RECORD (name, business unit, managed flag) and says nothing about what the role permits. Use dataverse_list_security_roles to find a role id by name.
Scope: this is the role's OWN privilege set. For a specific person's effective privileges across all their roles and teams use dataverse_retrieve_user_privileges, or dataverse_audit_user_access for the full access report.
RESPONSE SHAPE (verified live). Dataverse returns one top-level property, RolePrivileges, holding the whole list with no wrapper; privileges_source reports where the collection was found. Every entry carries all six of:
PrivilegeName — the familiar 'prvReadAccount' form. Already present on every entry, so NO extra lookup against the privileges table is made or needed.
PrivilegeId — GUID of the privilege.
Depth — the access level (see below).
BusinessUnitId — GUID of the business unit the depth is scoped to.
RecordFilterId, RecordFilterUniqueName — record-filter binding; empty on ordinary privileges.
Entries are passed through exactly as Dataverse sent them: nothing is added, renamed or dropped.
Depth arrives HUMAN-READABLE and is never relabelled. OData serializes the PrivilegeDepth enum as its member NAME, and only member names were observed live ("Basic", "Local", "Deep", "Global" — increasing scope, Global being org-wide; "Basic" is the user's own records). Should a numeric PrivilegeDepth code ever arrive instead, it is reported raw: that mapping is not confirmed for this function, and a wrong access-level label is more dangerous than an unlabelled one. depth_summary counts every entry by its Depth value.
THE LIST IS BIG AND IS TRIMMED BY DEFAULT. The function has no server-side paging — it returns every privilege in one response. Measured live: a System Administrator role carries 4,132 privileges in a ~1 MB raw response. That is why top defaults to 50 (~14 KB) and why the raw payload is never echoed back on the normalized path. The magnitude is never hidden: total_count is always the full number Dataverse returned regardless of trimming, has_more says whether anything was trimmed, and depth_summary is computed over ALL entries rather than just the returned page. Raise top (max 1000) to see more.
A well-formed but nonexistent role id returns an ERROR, not an empty list: Dataverse answers HTTP 404 [0x80040217] "Entity 'role' With Id = ... Does Not Exist", surfaced through the standard {"error": true, "message": ...} envelope. An empty privileges list therefore means a real role that grants nothing.
The function's inner properties are undocumented on Microsoft Learn, so the collection is still located by shape as well as by name (RolePrivileges first, then a lone object-list at the top level, then one level down inside a named wrapper) as insurance against a future platform change. If it cannot be identified unambiguously, nothing is guessed: normalized is false, no counts are reported, and the payload comes back unchanged under raw_response (minus the @odata.* envelope) for you to read yourself.
Inputs: params (required). Returns: result
dataverse_get_sdk_messageARead-onlyIdempotent
Resolve an SDK message (e.g. 'Create', 'Update', 'Delete') to its sdkmessageid.
Call this to get the message_id required by dataverse_create_plugin_step. Provide message_name OR message_id — exactly one required.
Inputs: params (required). Returns: result
dataverse_get_sdk_message_filterARead-onlyIdempotent
Resolve the filter that scopes an SDK message to one entity — returns the sdkmessagefilterid.
Call this to get the filter_id required by dataverse_create_plugin_step when you want to scope a step to a specific entity (e.g. 'contact'). Provide filter_id alone, or message_id + primary_entity together.
Inputs: params (required). Returns: result
dataverse_get_security_roleARead-onlyIdempotent
Retrieve a single Dataverse security role by its GUID.
Returns full role details including name, business unit, and managed status.
Inputs: params (required). Returns: result
dataverse_get_settingARead-onlyIdempotent
Read one setting's FINAL COMPUTED value for this environment.
Calls the unbound RetrieveSetting function, which returns the value actually in effect after the platform has applied its precedence rules, rather than a raw configuration row that only tells you what someone stored at one level. That makes it the tool for diffing configuration between environments: compare computed values, not rows.
It reads a NAMED setting from the settings framework, addressed by its unique name. It is not a general reader for the organization row: a column such as plugintracelogsetting is not a setting name, and dataverse_get_plugin_trace_log_setting (or dataverse_query_table over organizations) is what reads those.
Omit app_unique_name to read the ORGANIZATION-level value. Supply the unique name of a model-driven app to read the value as that app sees it, which can differ where an app-level override exists. The two are different requests: when app_unique_name is omitted the parameter is left out of the call entirely rather than sent empty.
THE VALUE IS NESTED. Microsoft Learn documents RetrieveSettingResponse but not its inner properties; live, v9.2 answers {"SettingDetail": {"Name": ..., "Value": "false", "DataType": 2}}. setting_value is lifted out of that container and setting_value_source says where it came from (normally SettingDetail.Value). setting_detail_name and setting_data_type carry its siblings; DataType is an INTEGER CODE passed through unmapped, since no verified code-to-type-name table exists. Note Value is a STRING — "false", not a JSON boolean — so parse it yourself rather than testing truthiness.
AN UNKNOWN SETTING NAME IS NOT AN ERROR. Dataverse answers HTTP 200 with SettingDetail: null. That is reported as setting_found: false with no setting_value, and it is a DIFFERENT answer from a setting that exists and holds "", "false" or 0 — those come back as setting_found: true with the value. Never read a missing setting_value as "the setting is off".
If the payload matches neither shape, setting_value is OMITTED rather than guessed and normalized is false. raw_response (minus the @odata.* envelope) always rides along on every path, so the extraction can be checked.
Setting names come from the settingdefinitions table (112 rows on a stock org), which dataverse_query_table can list. Both URL forms are live-verified to return HTTP 200: SettingName alone, and SettingName with AppUniqueName.
Inputs: params (required). Returns: result
dataverse_get_solutionARead-onlyIdempotent
Retrieve a single Dataverse solution by its unique name or GUID.
Returns full details including version, publisher, and managed status. Provide solution_unique_name or solution_id — not both.
Inputs: params (required). Returns: result
dataverse_get_solution_historyARead-onlyIdempotent
Retrieve a single solution history record by its GUID.
Returns import/upgrade/export operation details including result, timing, error messages, and publisher information from msdyn_solutionhistories.
The msdyn_suboperation field distinguishes operation sub-types:
3: Import/Update (in-place overlay; obsolete components are NOT deleted)
5: Upgrade-with-deletion (DeleteComponents phase; obsolete components ARE deleted)
Inputs: params (required). Returns: result
dataverse_get_table_metadataARead-onlyIdempotent
Get schema details for a single Dataverse table by logical name.
Returns the entity set name, primary key attribute, and primary name attribute. Use dataverse_list_tables to discover available table logical names.
Inputs: params (required). Returns: result
dataverse_get_teamARead-onlyIdempotent
Retrieve a single Dataverse team by its GUID.
Returns full team details including name, type, and business unit.
Inputs: params (required). Returns: result
dataverse_get_team_privilegesARead-onlyIdempotent
Answer "what can this TEAM actually DO?" — list a team's privileges.
Calls the entity-bound RetrieveTeamPrivileges function on the team record. It takes no parameters of its own: the team id is the key predicate.
This completes the three-way security picture. dataverse_get_role_privileges answers it for a ROLE, dataverse_retrieve_user_privileges for a USER, and this for a TEAM — the missing third. It is the companion to dataverse_get_team, which returns the team RECORD (name, type, business unit) and says nothing about what the team permits. Use dataverse_list_teams to find a team id by name, and dataverse_audit_user_access for one person's full access report across their direct roles and team memberships.
RESPONSE SHAPE. Microsoft Learn documents the call and the return type RetrieveTeamPrivilegesResponse but NOT its inner properties. VERIFIED LIVE: the collection arrives under RolePrivileges — NOT TeamPrivileges, despite the response type name — exactly as the sibling RetrieveUserPrivileges does. The collection is still located by name first (TeamPrivileges, which has never been observed, then RolePrivileges, which is what really comes back) and then by shape: a lone object-list at the top level, then one level down inside a named wrapper. privileges_source reports where it was found, so check it. If no collection can be identified unambiguously, nothing is guessed: normalized is false, no counts are reported, and the payload comes back unchanged under raw_response (minus the @odata.* envelope) for you to read yourself.
AN EMPTY LIST IS A REAL ANSWER, NOT A FAILURE. count: 0 with normalized: true means the team has NO DIRECTLY-ASSIGNED SECURITY ROLES — a common and entirely normal state, and the usual one: most teams get their access from their members' own roles rather than from a role assigned to the team itself. Do not read it as an error, and do not read it as "this team's members have no access": members still hold their own roles, and dataverse_audit_user_access is the tool for a person's effective access.
Entries mirror RetrieveRolePrivilegesRole's, VERIFIED LIVE for teams across 484 entries: PrivilegeName ('prvReadAccount'), PrivilegeId, Depth, BusinessUnitId, RecordFilterId, RecordFilterUniqueName — one identical key set on every entry, with PrivilegeName present and populated throughout, so no name-resolution step is needed. Entries are passed through EXACTLY as Dataverse sent them — nothing is added, renamed or dropped — so trust the returned keys over this list.
Depth is never relabelled. OData serializes the PrivilegeDepth enum as its member NAME, and this function was VERIFIED LIVE to return the member name — "Basic", "Local", "Deep", "Global" (increasing scope, Global being org-wide) — as a STRING on every one of 484 entries, with no numeric PrivilegeDepth code ever arriving. Should one nonetheless arrive it is reported raw rather than mapped: a wrong access-level label is more dangerous than an unlabelled one. depth_summary counts every entry by its Depth value, over the WHOLE list before any trimming.
This function and dataverse_get_role_privileges return the SAME privilege set for a team and its assigned role, but in a DIFFERENT ORDER — verified live as equal sets, unequal sequences. Never assume the two line up by index.
THE LIST CAN BE BIG AND IS TRIMMED BY DEFAULT. The function has no server-side paging — it returns every privilege in one response — and a team carrying a broad role inherits thousands of privileges (the role function was measured live at 4,132 privileges in a ~1 MB response). top therefore defaults to 50. The magnitude is never hidden: total_count is always the full number Dataverse returned, has_more says whether anything was trimmed, and depth_summary is computed over ALL entries rather than the returned page. Raise top (max 1000) to see more.
A well-formed but nonexistent team id returns an ERROR, not an empty list — VERIFIED LIVE: Dataverse answers HTTP 404 [0x80040217] "Does Not Exist", as the role function does, and it is surfaced through the standard {"error": true, "message": ...} envelope. The two cases are therefore distinguishable: an empty privileges list is always a REAL team with no directly-assigned roles, never a bad team id.
Inputs: params (required). Returns: result
dataverse_get_total_record_countsARead-onlyIdempotent
Get approximate row counts for many Dataverse tables in one round trip.
Calls the unbound RetrieveTotalRecordCount function with up to 50 table logical names (singular and lowercase — 'account', not 'accounts') and returns a {logical_name: count} map.
The counts come from a snapshot Dataverse takes at most once every 24 hours, so they are approximate and can lag reality by up to a day: a table populated an hour ago may report 0, and recent deletions may still be included. Worse, on an environment where the snapshot job has not run, EVERY count comes back 0 while the tables actually hold data (observed live on an org whose real counts were in the hundreds). The response flags that case with all_counts_zero=true — read it as "unknown", not "empty". Use this tool for cheap bulk sizing (which tables hold data, rough magnitudes, migration planning), and dataverse_count_records whenever an exact, live, or filtered count matters.
Unknown names are all-or-nothing, NOT silently dropped: a single logical name Dataverse does not recognize fails the whole call with HTTP 400 ([0x80040203] "Entity X was not found in the CRM system") and no partial results come back. Pass names you have already confirmed exist — dataverse_list_tables is the cheap way to confirm them. The error message names the offending table so you can drop it and retry.
If the response is not in the expected shape it is returned unchanged under raw_response with normalized=false rather than being guessed at.
Inputs: params (required). Returns: result
dataverse_get_userARead-onlyIdempotent
Retrieve a single Dataverse system user by their GUID.
Returns full user details including fullname, domainname, email, and disabled status. Use dataverse_whoami to get the current caller's UserId.
Inputs: params (required). Returns: result
dataverse_get_valid_relationship_entitiesARead-onlyIdempotent
List WHICH tables may take part in a relationship — the enumeration, before you pick a target.
Answers "which tables are eligible?" when you do not yet know what to point a lookup at. Its counterpart dataverse_check_relationship_eligibility answers "is THIS ONE table OK?" — a boolean about a table you can already name. The role values here mirror that tool's check_type values one for one, so use this to discover a candidate and that one to confirm a specific choice.
Call it before dataverse_create_one_to_many_relationship or dataverse_create_many_to_many_relationship: a table the platform excludes (many system and virtual tables) fails the create late and opaquely.
role selects one of three unbound functions:
'referenced' — tables that can be the PRIMARY (one) side of a 1:N, i.e. valid lookup TARGETS.
'referencing' — tables that can be the RELATED (many) side of a 1:N, i.e. tables that can HOLD a lookup.
'many_to_many' — tables that can participate in an N:N. Takes no table_logical_name; supplying one is an input error rather than being ignored, because ignoring it would answer the environment-wide question while looking scoped.
table_logical_name DOES NOT NARROW THE ANSWER. It is optional for the two 1:N roles and Dataverse does validate it server-side (an unknown table is HTTP 400 [0x80041102] "not found in the MetadataCache"), but supplying it was measured live to return a BYTE-IDENTICAL list to omitting it, for every table tried. Every role therefore answers the environment-wide question: the tables eligible for that role at all. Passing a name buys you exactly one thing — proof the table exists — so pass it only when you want that check, and never read the result as "the tables THIS table may point at". This holds for CUSTOM tables as well as system ones: scoping by a custom table returned the same byte-identical 575-name list that 'account' and 'systemuser' did. The response says so explicitly via table_logical_name_filtered.
To ask about one specific table, use dataverse_check_relationship_eligibility, which returns a real per-table boolean. This tool cannot answer that question.
Supply a lowercase logical name ('account', not 'accounts'). Omitting it removes the parameter from the call entirely rather than sending an empty one — the two are different requests, even though they answer the same.
THE ANSWER IS BIG and 'referenced' is the biggest. Measured live: referenced 575 names / ~13 KB, many_to_many 305 / ~7 KB, referencing 166 / ~4 KB. None of these functions pages server-side, so names are trimmed to top (default 250) while count, total_count and has_more always describe the full set Dataverse returned.
The list is returned under EntityNames (live-confirmed for all three functions), which is tried first, then a by-shape fallback (a sole top-level list of strings); source names where it was actually found. If it cannot be located unambiguously, table_logical_names and the counts are OMITTED, normalized is false, and the untouched body is returned under raw_response — an unreadable payload is never reported as an empty list.
An empty list from a readable payload IS a real answer: it means no table qualifies for that role. A nonexistent table name is an HTTP error, not an empty list.
Inputs: params (required). Returns: result
dataverse_get_viewARead-onlyIdempotent
Get a single Dataverse view's layout as structured JSON.
Parses FetchXml and LayoutXml into readable columns, sort, and filter lists. Returns fetchxml_backup and layoutxml_backup for reference. quick_find_fields is populated for Quick Find views (querytype=4). Use dataverse_list_views to discover view IDs.
Inputs: params (required). Returns: result
dataverse_get_web_resourceARead-onlyIdempotent
Retrieve a single webresource record by its GUID.
Returns webresourceid, name, displayname, webresourcetype (+ label), description, languagecode, ismanaged, iscustomizable, createdon, modifiedon. Set include_content=true to also retrieve the base64-encoded content field (may be large for images and script bundles — a 5 MB cap applies).
IMPORTANT: this returns the PUBLISHED version of the web resource. Edits made via dataverse_update_web_resource are saved to the unpublished draft and will NOT appear here until you call dataverse_publish_customizations for this web resource. (Use dataverse_retrieve_unpublished with entity_set_name='webresourceset' to read the draft before publishing.)
Inputs: params (required). Returns: result
dataverse_is_component_customizableARead-onlyIdempotent
Check whether a solution component can be customized BEFORE trying to edit it.
Calls the unbound IsComponentCustomizable function. Nothing is read or modified — it is a cheap pre-flight check for the metadata write tools (dataverse_update_table, dataverse_update_column, dataverse_update_relationship, dataverse_update_choice, dataverse_delete_column, ...), which otherwise fail late and opaquely when the target belongs to a managed solution that locked it down. Run this first when editing anything you did not create yourself.
Pass the component's own GUID (a table's or column's MetadataId, a form's or web resource's record id — NOT a solution id) plus the integer component_type code that says what the GUID refers to. The codes are the same set dataverse_analyze_dependencies uses (1=Entity, 2=Attribute, 3=Relationship, 9=OptionSet, 60=SystemForm, 61=WebResource, 300=CanvasApp, ...); the resolved name is echoed back as component_type_name so a mismatched code is easy to spot.
The verdict is returned as a top-level is_customizable boolean. Dataverse's response was verified live and is flat, carrying exactly one property named after the function itself — {"IsComponentCustomizable": true} — which is read directly; is_customizable_source names the property the value came from. If a future platform version answers in some other shape, a lone boolean anywhere in the payload is still accepted as a fallback, and when no verdict can be identified unambiguously the key is OMITTED rather than guessed or returned as null, with normalized false and a message saying so. Never read a missing is_customizable as false. The payload is always echoed unchanged under raw_response (minus the @odata.* envelope).
A false answer means the component belongs to a managed solution whose publisher locked it down. A true answer is not a guarantee that every edit will succeed: individual managed properties (for example IsRenameable or IsValidForAdvancedFind) can still block a specific change on an otherwise customizable component.
DO NOT ASSUME SYSTEM COMPONENTS ANSWER false. Core out-of-the-box tables report true (systemuser, component type 1, was verified as true) because the platform permits customizations such as adding columns even though the base asset itself is managed. The tool does discriminate — a managed web resource (type 61) returned false while an unmanaged one returned true.
A well-formed GUID that matches no component is an HTTP 400 carrying [0x80040216] "There should be at least one metadata entity returned for EntityName: ...", surfaced through the standard {"error": true, "message": ...} envelope: it means the component id (or the component_type paired with it) is wrong, not that the component is locked.
Inputs: params (required). Returns: result
dataverse_list_alternate_keysARead-onlyIdempotent
List alternate keys (EntityKeyMetadata) defined on a Dataverse table.
Alternate keys let integration tools upsert records by business values instead of GUIDs. Returns SchemaName, LogicalName, KeyAttributes, and EntityKeyIndexStatus (which tracks async index build progress). Use the LogicalName with dataverse_delete_alternate_key to remove a key.
Inputs: params (required). Returns: result
dataverse_list_appsARead-onlyIdempotent
List model-driven apps (AppModule records) in a Dataverse environment.
Returns appmoduleid, name, uniquename, description, publish state, and statecode. Set include_unpublished=true to also return draft apps not yet visible to users. Use dataverse_get_app to inspect a single app's components.
Inputs: params (required). Returns: result
dataverse_list_async_operationsARead-onlyIdempotent
List asyncoperation (system job) records in the Dataverse environment.
Returns asyncoperationid, name, operationtype, statecode, statuscode, message, friendlymessage, startedon, completedon, createdon, modifiedon, and _regardingobjectid_value. Also includes statecode_label and statuscode_label for human readability. Filter by state_code (0=Ready,1=Suspended,2=Locked,3=Completed), status_code (0=WaitingForResources,10=Waiting,20=InProgress,21=Pausing, 22=Canceling,30=Succeeded,31=Failed,32=Canceled), or operation_type (raw int). Use dataverse_get_async_operation for full details on a specific job.
Inputs: params (required). Returns: result
dataverse_list_auditARead-onlyIdempotent
Query the audit table with optional OData filters.
Returns audit records from the 'audits' entity set. Common columns:
auditid, createdon — record identity and timestamp
operation — 1=Create, 2=Update, 3=Delete, 4=Access, 5=Upsert
action — specific event code (e.g., 1=Create, 2=Update, 3=Delete, 64=User Access via Web, 65=User Access via Web Services)
objecttypecode — logical name of the audited entity (e.g., 'account')
_userid_value — GUID of the user who made the change
_objectid_value — GUID of the audited record
transactionid — groups related changes in one operation
Use dataverse_get_audit_details to fetch full before/after values for a specific audit record.
Note: requires auditing enabled on the org. If auditing is disabled, Dataverse may return an empty result set or an HTTP error.
Inputs: params (required). Returns: result
dataverse_list_business_unitsARead-onlyIdempotent
List business units in the Dataverse environment.
Returns businessunitid, name, parent business unit, disabled flag, and modifiedon. Use filter to narrow results (e.g., "isdisabled eq false").
Inputs: params (required). Returns: result
dataverse_list_choice_column_optionsARead-onlyIdempotent
Get option values and labels for a Picklist or MultiSelectPicklist column's LOCAL option set.
Use this before filtering records with choice columns — the integer value is required for OData filter expressions (e.g., "statuscode eq 1"). For GLOBAL choices shared across tables use dataverse_get_choice instead. Handles both Picklist and MultiSelectPicklist automatically.
Inputs: params (required). Returns: result
dataverse_list_choicesARead-onlyIdempotent
List GLOBAL choice (option set) definitions in the Dataverse environment.
Option values and labels are not returned here — use dataverse_get_choice to retrieve the full option set for a specific choice. $filter is not supported by this endpoint; top is applied client-side.
Inputs: params (required). Returns: result
dataverse_list_columnsARead-onlyIdempotent
List column (attribute) definitions for a Dataverse table.
Use attribute_type to narrow by column type (e.g., 'Lookup', 'Picklist'). For full metadata on a single column use dataverse_get_column. For Picklist/MultiSelectPicklist option values use dataverse_list_choice_column_options.
Inputs: params (required). Returns: result
dataverse_list_connection_referencesARead-onlyIdempotent
List connection references in the Dataverse environment.
Returns connectionreferenceid, logical name, display name, connector ID, connection ID (empty string if not yet assigned), status, and managed state.
Filter by connector_id to find all references for a specific connector type. Filter by statecode=0 to show only active references. Use the connection_id field to identify which references still need a connection assigned — an empty connectionid means the flow or app using it will fail at runtime.
Inputs: params (required). Returns: result
dataverse_list_custom_api_request_parametersARead-onlyIdempotent
List request parameter records for a Custom API.
Filters by custom_api_id (GUID) to return only parameters belonging to that Custom API. Returns customapirequestparameterid, uniquename, name, displayname, type, isoptional for each record. Type enum: 0=Boolean, 1=DateTime, 2=Decimal, 3=Entity, 4=EntityCollection, 5=EntityReference, 6=Float, 7=Integer, 8=Money, 9=Picklist, 10=String, 11=StringArray, 12=Guid. An optional OData filter expression can further narrow results.
Inputs: params (required). Returns: result
dataverse_list_custom_api_response_propertiesARead-onlyIdempotent
List response property records for a Custom API.
Filters by custom_api_id (GUID) to return only properties belonging to that Custom API. Returns customapiresponsepropertyid, uniquename, name, displayname, type for each record. Type enum: 0=Boolean, 1=DateTime, 2=Decimal, 3=Entity, 4=EntityCollection, 5=EntityReference, 6=Float, 7=Integer, 8=Money, 9=Picklist, 10=String, 11=StringArray, 12=Guid. An optional OData filter expression can further narrow results.
Inputs: params (required). Returns: result
dataverse_list_custom_apisARead-onlyIdempotent
List Custom API records in the Dataverse environment.
Returns customapiid, uniquename, name, displayname, bindingtype, isfunction, isprivate, and allowedcustomprocessingsteptype for each record. Use the optional filter parameter to narrow results with an OData $filter expression (e.g., "isprivate eq false", "isfunction eq true"). bindingtype: 0=Global, 1=Entity, 2=EntityCollection. allowedcustomprocessingsteptype: 0=None, 1=AsyncOnly, 2=SyncAndAsync. Use dataverse_get_custom_api to fetch expanded request parameters and response properties for a specific Custom API.
Inputs: params (required). Returns: result
dataverse_list_environmentsARead-onlyIdempotent
List Power Platform environments available to the authenticated user.
Uses the Power Platform admin API — no dataverse_url required. Returns instance_url for each environment, which is the dataverse_url for all other tools. Use this to discover environments before calling environment-specific Dataverse tools.
Inputs: params (required). Returns: result
dataverse_list_formsARead-onlyIdempotent
List model-driven app forms registered in the Dataverse environment.
Returns form metadata: id, name, type, activation state, and default flag. Filter by table_logical_name and/or form_type (2=Main, 4=Quick View, 5=Quick Create, 9=Card). Use dataverse_get_form to inspect a form's layout.
Inputs: params (required). Returns: result
dataverse_list_import_jobsARead-onlyIdempotent
List importjob records, optionally filtered by solution unique name.
The large result XML ('data' column) is excluded from all records by default. Results are ordered by createdon descending (most recent first).
Inputs: params (required). Returns: result
dataverse_list_languagesARead-onlyIdempotent
List an environment's language codes (LCIDs) — which are usable, which are merely installed.
Call this before writing any localized label. provisioned is the load-bearing answer: those are the LCIDs that are actually ENABLED in this environment and therefore the only ones a LocalizedLabels entry may use (the LanguageCode on a DisplayName / Description label passed to dataverse_create_table, dataverse_create_column, dataverse_create_choice, ...). A LocalizedLabel for a language that is installed but NOT provisioned is rejected or silently dropped, so never assume 1033 (English) is provisioned — verify it.
Three unbound, zero-parameter functions are called CONCURRENTLY and their answers reconciled. THE THREE SETS CAN BE MUTUALLY DISJOINT — on the org this tool was verified against, available was [1033], provisioned was [1033], and installed_packs held 44 OTHER LCIDs not including 1033. Read each for what it literally reports and do not infer one from another:
provisioned (RetrieveProvisionedLanguages) — the LCIDs enabled for use in this environment. THE LOAD-BEARING ONE: this is the set a LocalizedLabels entry may use, and the only one worth deciding anything from.
available (RetrieveAvailableLanguages, container LocaleIds) — what this function reports the environment as offering. Observed live as a SHORT list that matched provisioned exactly and shared nothing with installed_packs; it is NOT "every language whose pack is on the server".
installed_packs (RetrieveInstalledLanguagePacks) — the language packs present on the server. Observed live as by far the largest of the three and DISJOINT from both of the others. A pack being installed does not make its LCID provisioned, and this tool does not know which of them could be.
Each list is echoed exactly as Dataverse sent it (order included, not sorted) with a matching *_count. Two derived diffs are computed only when both of their inputs were read successfully, and are named after the exact subtraction they perform rather than implying one is the actionable answer: available_not_provisioned (available minus provisioned) and installed_not_provisioned (installed_packs minus provisioned). Neither is a list of languages an administrator can simply turn on — provisioning has its own prerequisites — so treat both as leads to investigate, not as a to-do list.
THE THREE CALLS FAIL INDEPENDENTLY. A function that is unavailable or privilege-gated is reported in partial_errors and the others' data is still returned, so ALWAYS read partial_errors before concluding a language is absent — a missing key means "not answered", never "empty". Only a failure of all three yields the standard {"error": true, "message": ...} envelope.
The three container property names genuinely differ (LocaleIds for the available list; the function's own name for the other two) and all three are LIVE-CONFIRMED. Each is still tried by name and then by shape (a sole list of integers), so a future change degrades rather than lies. sources reports the property each list was actually found under — check it. If a payload cannot be read unambiguously, its keys are OMITTED, normalized is false, and the untouched body appears under raw_responses; nothing is fabricated and no empty list is invented.
LCIDs are Windows locale ids (1033 = English (United States), 1036 = French (France), 1031 = German (Germany), 3082 = Spanish (Spain)); they are returned as raw integers and are not mapped to language names here.
Inputs: params (required). Returns: result
dataverse_list_plugin_assembliesARead-onlyIdempotent
List plug-in assemblies registered in the environment, optionally filtered.
Results ordered newest-modified first. Use package_id to scope to one plug-in package. The prerequisite chain is: assembly (or package) → plug-in type → processing step → step image.
Inputs: params (required). Returns: result
dataverse_list_plugin_packagesARead-onlyIdempotent
List NuGet-based plug-in packages registered in the environment.
Results ordered newest-modified first. Packages are an alternative to raw assemblies as the first step in the prerequisite chain.
Inputs: params (required). Returns: result
dataverse_list_plugin_step_imagesARead-onlyIdempotent
List plug-in step images (pre/post entity snapshots) registered against steps.
Use step_id to scope to one processing step.
Inputs: params (required). Returns: result
dataverse_list_plugin_stepsARead-onlyIdempotent
List SDK message processing steps registered in the environment.
Use plugin_type_id to scope to one plug-in type, or message_id to scope to one message. Results ordered by name ascending.
Inputs: params (required). Returns: result
dataverse_list_plugin_trace_logsARead-onlyIdempotent
List plug-in trace log records with optional filters.
Returns trace and exception information generated by plug-ins and custom workflow activities. Records are ordered newest-first.
Useful filters:
type_name: partial match on the plug-in class name (typename)
exceptions_only: true to show only failed executions
hours_ago: narrow to recent logs (e.g. 1 for the last hour)
message_name: filter by triggering message (e.g. 'Create', 'Update')
primary_entity: filter by entity the plug-in ran against
Plug-in trace logging must be enabled via dataverse_set_plugin_trace_log_setting before logs will be generated. Use dataverse_get_plugin_trace_log_setting to check the current setting.
Inputs: params (required). Returns: result
dataverse_list_plugin_typesARead-onlyIdempotent
List plug-in types (.NET classes) registered in the environment.
Use assembly_id to scope to one assembly. Results ordered by typename ascending. Types are the second step in the prerequisite chain: assembly → type → step → image.
Inputs: params (required). Returns: result
dataverse_list_plugin_type_statisticsARead-onlyIdempotent
List runtime performance statistics for Dataverse plug-in types.
Returns execution counts, failure rates, crash metrics, and worker-process termination contribution percentages per plug-in type. Records are populated by Dataverse within 30–60 minutes of plug-in execution; all fields are read-only.
Filter to a specific plug-in type with plugin_type_id, or omit to return statistics for all plug-in types. Set include_plugin_type_details=true to expand each row with the plug-in type name, typename, and assemblyname.
Use this to identify slow, high-failure, or crash-prone plug-ins before investigating further with the Power Platform Admin Center analytics dashboard.
Inputs: params (required). Returns: result
dataverse_list_privilegesARead-onlyIdempotent
List the privileges DEFINED in the environment — the catalogue of what CAN be granted.
This is the reference list, not an assignment. dataverse_get_role_privileges, dataverse_get_team_privileges and dataverse_retrieve_user_privileges answer "who HOLDS what"; this answers "what privileges exist, what access right does each carry, and at which depths can it be granted". Use it to look up the privilege behind a name those tools return ('prvReadAccount'), or to enumerate everything that exists for one table.
ACCESS RIGHTS ARE DECODED BY A HAND-ROLLED MAP, AND THAT IS THE POINT. The accessright column is an integer with NO option set behind it anywhere in Dataverse: the PicklistAttributeMetadata cast 404s, GlobalOptionSetDefinitions for it 404s, and annotation-included FormattedValues return only the integer with thousands separators. So access_right_name comes from a map derived empirically and cross-checked across every privilege in the environment: 0 None · 1 ReadAccess · 2 WriteAccess · 4 AppendAccess · 16 AppendToAccess · 32 CreateAccess · 65536 DeleteAccess · 262144 ShareAccess · 524288 AssignAccess The gaps are real (8 and 16-32768 are unused), so a name is never derived by shifting bits. AN UNRECOGNISED VALUE IS REPORTED RAW: access_right still carries it, access_right_name is ABSENT, and the value is listed under unmapped_access_rights. Nothing is invented — a wrong access-level label is more dangerous than an unlabelled one, the same discipline dataverse_get_team_privileges applies to Depth. accessright 0 marks the non-CRUD privileges (prvActOnBehalfOf... and friends); it is a real value, not "unknown".
depths COLLAPSES THE FOUR canbe* FLAGS into one ordered list, e.g. ["Basic","Local","Deep","Global"] — the depths at which that privilege may be granted, by increasing scope (Basic = the user's own records, Global = org-wide). Only six combinations exist in practice and nearly every privilege allows Global. An EMPTY depths list is unexpected and means the flags could not be read, not that the privilege can be granted nowhere.
total_count COMES FROM AN AGGREGATION, NOT @odata.count. On this collection @odata.count CAPS AT 5,000 and lies — ?$count=true reports 5,000 where the true catalogue is ~7,346 — so the count is taken with $apply=aggregate($count as c), which bypasses the cap. If a trustworthy total cannot be obtained, total_count is OMITTED and message says so; a capped number is never reported as the truth. count is the size of the returned page, has_more says whether anything was trimmed.
TABLE SCOPING GOES THROUGH A JOIN TABLE, NOT THROUGH PRIVILEGE NAMES. Passing table_logical_name queries privilegeobjecttypecodesset, whose objecttypecode column holds the table's LOGICAL NAME STRING. Filtering by name instead — endswith(name,'Account') — is WRONG in general even though it looks right on the tables people test with: endswith(name,'Role') returns 25 privileges spanning FOUR different tables (role, connectionrole, relationshiprole, mspp_webrole). Privileges are also many-to-many with tables (one privilege can map to as many as 14), which a name can never express. That join table is private and undocumented, so if it fails you get a clear error naming it — never a silent fall back to name matching.
AN UNKNOWN TABLE NAME IS AN ERROR, NOT AN EMPTY LIST. objecttypecode is an EntityName column, so Dataverse validates it: an unknown, misspelled or plural logical name answers HTTP 400 [0x80041102] "The entity with a name = '…' with namemapping = 'Logical' was not found in the MetadataCache", naming the offending entity — that message is the reliable signal for a bad table name, and it is what the error surfaces first. Casing is never the cause: table_logical_name is lowercased for you, matching name_startswith's case-insensitivity on both routes. An EMPTY privileges list means the opposite — the table EXISTS and genuinely has no privileges mapped to it, live-confirmed on 'privilege' itself, which returns 0.
source names the route that actually ran ('privileges' or 'privilegeobjecttypecodesset'); the response shape is identical either way. On the join route, name_startswith and access_right are applied client-side, so count/total_count describe the filtered set.
Each entry: name, privilege_id, access_right (raw integer), access_right_name (absent when unknown), depths, can_be_entity_reference, can_be_parent_entity_reference. Bulky and empty columns are deliberately dropped (privilegetype does not exist on this entity at all).
If the response carries no readable collection, nothing is guessed: normalized is false, no counts are reported, and the body comes back under raw_response. An empty privileges list with normalized: true is a real answer; a missing container is not.
Inputs: params (required). Returns: result
dataverse_list_processesARead-onlyIdempotent
List classic processes (workflows, business rules, actions, BPFs) from the workflow entity.
Classic process categories: 0=Workflow, 1=Dialog, 2=Business Rule, 3=Action, 4=Business Process Flow. Use the category parameter to filter by a specific category. Cloud flows (category 5) are excluded by default unless you explicitly set category=5.
By default returns only type=1 (definition) records to avoid duplicate activation/template rows. Set type=None to return both types.
Use filter for additional OData conditions (e.g., "statecode eq 1" for activated processes only, or "primaryentity eq 'account'").
Inputs: params (required). Returns: result
dataverse_list_relationshipsARead-onlyIdempotent
List relationship definitions for a table (OneToMany, ManyToOne, ManyToMany) or the whole environment.
Use the returned SchemaName with dataverse_get_relationship for full cascade and navigation property details. Navigation property names from the results are required for OData $expand queries and for dataverse_associate_records.
Inputs: params (required). Returns: result
dataverse_list_sdk_message_filtersARead-onlyIdempotent
List SDK message filters showing which entities support each message.
Use message_id to scope to one message, or primary_entity to see all messages supported by one table. Use dataverse_get_sdk_message_filter to resolve a specific filter_id for dataverse_create_plugin_step.
Inputs: params (required). Returns: result
dataverse_list_sdk_messagesARead-onlyIdempotent
List SDK messages — the catalog of operations plug-in steps can intercept.
Use to discover valid message names (e.g. 'Create', 'Update', 'Assign') before calling dataverse_get_sdk_message to resolve message_id for a step.
Inputs: params (required). Returns: result
dataverse_list_security_rolesARead-onlyIdempotent
List security roles in the Dataverse environment.
Returns roleid, name, businessunitid, managed status, and modifiedon. Use filter to narrow results (e.g., "ismanaged eq false"). Use dataverse_get_security_role for full details on a specific role.
Inputs: params (required). Returns: result
dataverse_list_solution_componentsARead-onlyIdempotent
List components within a Dataverse solution, with human-readable type names.
Use component_type to filter by type code (1=Entity, 61=Web Resource, 300=Canvas App, 91=Plugin Assembly, 92=SDK Message Processing Step).
Inputs: params (required). Returns: result
dataverse_list_solution_historiesARead-onlyIdempotent
List solution history records from msdyn_solutionhistories.
Tracks import, upgrade, and export operations on solutions. Optionally filter by solution_id or solution_unique_name (mutually exclusive). solution_id is resolved to the solution unique name first, then used to filter history records via msdyn_name. Omit both to list all.
The msdyn_suboperation field distinguishes operation sub-types:
3: Import/Update (in-place overlay; obsolete components are NOT deleted)
5: Upgrade-with-deletion (DeleteComponents phase; obsolete components ARE deleted)
Use msdyn_suboperation to determine whether a history record represents a standard update or a true upgrade with component deletion.
Inputs: params (required). Returns: result
dataverse_list_solutionsARead-onlyIdempotent
List solutions in the Dataverse environment with name, version, and managed status.
Use filter to narrow results (e.g., "ismanaged eq false"). Use dataverse_get_solution for full details on a specific solution.
Inputs: params (required). Returns: result
dataverse_list_tablesARead-onlyIdempotent
List tables (entities) in the Dataverse environment with their logical names and display names.
Use filter to narrow results (e.g., "IsCustomEntity eq true" for custom tables only). Use dataverse_get_table_metadata for full schema details on one table. Use dataverse_get_entity_sets to discover OData collection names for record queries.
Inputs: params (required). Returns: result
dataverse_list_teamsARead-onlyIdempotent
List teams in the Dataverse environment.
Returns teamid, name, teamtype, businessunitid, and modifiedon. Use filter to narrow results (e.g., "teamtype eq 0" for owner teams). Use dataverse_get_team for full details on a specific team.
Inputs: params (required). Returns: result
dataverse_list_usersARead-onlyIdempotent
List system users (systemusers) in the Dataverse environment.
Returns systemuserid, fullname, domainname, email, disabled flag, and businessunitid. Use filter to narrow results (e.g., "isdisabled eq false", "domainname eq 'user@contoso.com'"). Use dataverse_get_user for full details on a specific user.
Inputs: params (required). Returns: result
dataverse_list_viewsARead-onlyIdempotent
List saved views (savedqueries) registered in the Dataverse environment.
Returns metadata: id, name, querytype, isdefault, statecode. Filter by table_logical_name and/or query_type (0=Main Grid, 1=Advanced Find, 2=Associated, 4=Quick Find, 64=Lookup). Use dataverse_get_view for layout.
Inputs: params (required). Returns: result
dataverse_list_web_resourcesARead-onlyIdempotent
List webresource records in the Dataverse environment.
Returns webresourceid, name, displayname, webresourcetype (+ label), description, languagecode, ismanaged, iscustomizable, createdon, modifiedon. Content is excluded from list results (it can be very large); use dataverse_get_web_resource with include_content=true to retrieve it. Filter by web_resource_type (1=HTML, 2=CSS, 3=JScript, 4=XML, 5=PNG, 6=JPG, 7=GIF, 8=XAP, 9=XSL, 10=ICO, 11=SVG, 12=RESX) and/or name_contains for a case-sensitive substring match on the name field. After creating or updating a web resource, call dataverse_publish_customizations to make changes live.
Inputs: params (required). Returns: result
dataverse_query_tableARead-onlyIdempotent
Query records from a Dataverse table with OData filtering, ordering, and expansion.
For a single record by GUID use dataverse_get_record. For just a count use dataverse_count_records. For group-by aggregation use dataverse_aggregate_table. To create, update, or delete records use dataverse_create_record, dataverse_update_record, or dataverse_delete_record.
Always specify select to limit returned columns and keep payloads small.
Inputs: params (required). Returns: result
dataverse_retrieve_access_originARead-onlyIdempotent
Answer "WHY does this principal have access to this record?".
Calls the unbound RetrieveAccessOrigin function, which explains where a principal's rights over one specific row come from — object ownership, or the Principal Object Access (POA) table that backs explicit shares and team or hierarchy grants.
This is the companion to dataverse_retrieve_principal_access, which returns only the access MASK (which rights: Read, Write, Delete, …) and cannot say where those rights came from. When you are debugging "why can this user see this record?" or "why can't they?", the mask is the symptom and this is the cause. Use dataverse_audit_user_access for the wider picture (roles, teams, effective privileges) and dataverse_get_role_privileges for what one role permits in general rather than on one row.
Inputs:
object_id — the record's own GUID.
logical_name — the SINGULAR lowercase logical name of that record's table ('account', not 'accounts'). This is deliberately not the entity set name the record-access tools take.
principal_id — a systemuser id or a team id. No other principal type is accepted; use dataverse_list_users / dataverse_list_teams.
RESPONSE SHAPE (verified live). Dataverse answers with ONE scalar string property, Response — never a collection, in a raw body of roughly 286 bytes. It is surfaced as access_origin, with access_origin_source naming the property it was read from, and the payload (minus the @odata.* envelope) rides along under raw_response so you can check that for yourself. There is no count: the answer is never list-shaped. Should a future platform change move the answer somewhere unrecognizable, normalized is false, nothing is fabricated, and raw_response is the whole answer.
HTTP 200 DOES NOT MEAN "HAS ACCESS" — READ THE STRING. Three materially different outcomes all come back as a successful call with normalized true, and they are distinguishable ONLY by the English prose inside the string. The text is passed through verbatim and deliberately NOT classified into a boolean: pattern-matching platform prose is fragile and locale-dependent, and a wrong security verdict is worse than none. Observed live in ONE org — these wordings are observations, not a documented platform contract, so treat the list as incomplete and never match on it:
Access exists, with the reason. Two forms seen, both meaning "owner" — "PrincipalId is object owner ()" on a user- or team-owned row, and "PrincipalId is member of organization () who is object owner ()" on an ORGANIZATION-owned row (see the org-owned note below for why that answer is the same for every principal).
NO access at all — "Access origin could not be found. Access does not come from POA table or object ownership."
The record DOES NOT EXIST — still HTTP 200, carrying the platform's "Does Not Exist" exception text inside the Response string. A bad object_id is NOT a 404 from this function, so an unread string looks exactly like a successful answer. Do not report that the principal has access unless the string says so.
Other live-confirmed behaviour:
An unknown but grammar-valid logical_name is a clean HTTP 400 [0x80041102] "... was not found in the MetadataCache", surfaced through the standard {"error": true, "message": ...} envelope. Confirm the name with dataverse_list_tables.
On an ORGANIZATION-owned table (solution, role, …) the answer is the same for every principal, because ownership resolves at organization level. That is correct platform behaviour, not a defect — discrimination between principals shows up on user- and team-owned rows.
A nonexistent principal_id is not validated against an org-owned row: it returned the same generic ownership text as a real one. Confirm the principal exists with dataverse_get_user / dataverse_get_team first.
Inputs: params (required). Returns: result
dataverse_retrieve_principal_accessARead-onlyIdempotent
Return the access rights a system user has to a specific Dataverse record.
Returns the AccessRights bitmask and named rights (ReadAccess, WriteAccess, DeleteAccess, etc.). Use before delegating an operation to confirm the user can act on the record.
Inputs: params (required). Returns: result
dataverse_retrieve_record_change_historyARead-onlyIdempotent
Retrieve the full audit change history for a specific record.
Calls the unbound RetrieveRecordChangeHistory function, which returns the same AuditDetailCollection container as its column-scoped sibling dataverse_get_attribute_change_history — the entries sit TWO levels down (AuditDetailCollection -> AuditDetails), not one.
AN HTTP 200 IS NOT PROOF OF ANYTHING, AND ON A 404 READ THE ERROR CODE. Live- confirmed on this function, and the earlier note that auditing being off produces an HTTP error was WRONG:
auditing disabled at organization/table level returns HTTP 200 carrying the audit-configuration rows described below and ZERO genuine changes — not an error, and no error message to read;
MOST tables do NOT validate that the target record exists: a well-formed but NONEXISTENT record id with the CORRECT plural entity set returns HTTP 200 with zero genuine changes (12 of 15 entity sets swept on one org behaved this way, 'accounts' among them). An empty answer is therefore never evidence that the record is there, or that it never changed;
a 404 is NOT automatically a naming mistake — the error CODE decides. [0x80060888] "Resource not found for the segment ''" NAMES the bad segment: the entity set is wrong (typically the singular slipped in for the plural) or that table is not provisioned on this org, so fix the name with dataverse_get_entity_sets rather than hunt a deleted row. [0x80048d02] has been seen instead from a CORRECT plural entity set ('audits') and there means what it says — the row really is absent. So some entity sets DO validate the target. One org and 15 entity sets were swept, so treat neither group as a complete list and read the code that actually came back. Use dataverse_get_attribute_change_history when the question is about one column; it additionally diagnoses which level auditing is switched off at.
NOT EVERY ENTRY IS A RESULT. Dataverse MAY add org-level audit-CONFIGURATION rows (records of auditing itself being switched on or off) to a response. They arrive when an audit-configuration change falls inside the TARGET RECORD'S history window, so their presence and count VARY BY TARGET — a record created after the last such change gets none, while older records on the same org got four each, live-measured. audit_configuration_events_count: 0 is a normal, expected answer. They are identified by their SHAPE — no @odata.type, AuditRecord and nothing else, and an all-zero AuditRecord._objectid_value — never by their position, which is not a contract. They are split out into audit_configuration_events (with audit_configuration_events_count) and are NOT counted: audit_details, count and has_more cover this record's own changes only.
ENTRIES ARE POLYMORPHIC — read each one's @odata.type, and detail_types counts the values present on the returned page. A RECORD-scoped call spans everything that happened to the record, so expect a wider mix than a column-scoped one: AttributeAuditDetail (OldValue/NewValue per changed field), RelationshipAuditDetail, ShareAuditDetail (live-confirmed here), RolePrivilegeAuditDetail and UserAccessAuditDetail are all documented subtypes. Every subtype carries an AuditRecord navigation property (who, when, what operation). An entry with an UNRECOGNIZED @odata.type is reported as a change, never quietly dropped, and unclassified_typeless_count reports how many entries carrying NO @odata.type were kept as changes because they did not match the configuration shape — it is 0 on every response observed so far, and a non-zero value means this tool met an entry it could not name rather than that anything was lost.
RESPONSE SHAPE IS CHECKED, NOT ASSUMED. If the AuditDetailCollection container is absent or is not a list of entries, the tool returns normalized: false with the raw body — a missing container is NOT reported as "no changes".
PagingInfo is not sent, so changes are trimmed client-side to top and has_more reports the server's MoreRecords OR anything the trim cut. total_record_count appears ONLY when Dataverse supplied a real count: it is live-confirmed to arrive as -1 here ("not counted"), and a negative value is suppressed rather than passed on as a number that reads like a count.
URL form: GET /api/data/v9.2/RetrieveRecordChangeHistory(Target=@p1) ?@p1={'@odata.id':'<entity_set_name>(<record_id>)'}
Inputs: params (required). Returns: result
dataverse_retrieve_unpublishedARead-onlyIdempotent
Read the UNPUBLISHED (draft) definition of one customization record.
A normal GET — and therefore dataverse_get_form, dataverse_get_view and dataverse_get_web_resource — returns the PUBLISHED row. Writes such as dataverse_set_formxml, dataverse_add_form_control, dataverse_update_view and dataverse_add_view_column save to the draft, so after any of them the published read is stale until dataverse_publish_customizations runs. Call this tool to read back what you just wrote; reading the published row and then editing it can silently clobber your own unpublished changes.
Supported entity_set_name values: 'savedqueries' (views), 'systemforms' (forms), 'appmodules', 'webresourceset'. Dataverse accepts the RetrieveUnpublished message for only certain customization entity types, and sitemap is NOT one of them — a sitemap draft cannot be read this way, so do not go looking for it. Ordinary data tables such as 'accounts' have no unpublished layer at all.
Returns one record, not a list. By default a small projection is returned with the large XML/binary columns held back (formxml, fetchxml, layoutxml, content) — pass select to ask for them explicitly, e.g. select=['formid','name','formxml']. select is honoured and validated: an unknown column name comes back as an HTTP 400 naming the property.
IMPORTANT — the returned column set is NOT the set you asked for, and it differs in BOTH directions.
FEWER: unlike a plain GET, RetrieveUnpublished omits a requested column whose value is NULL instead of returning it as null. A missing key means "this column is null", NOT "this column does not exist".
MORE: the platform also returns columns you never requested. It is an open-ended set, not one known extra — _organizationid_value comes back on every entity set, and a narrow select on 'systemforms' also returned objecttypecode and type. Treat any unrequested column as possible. So never assume returned set == requested set, and do NOT diff requested against returned to detect a mistyped select column: nulls vanish from that diff and platform extras pollute it. The reliable signal for a bad column name is Dataverse's own HTTP 400 "Could not find a property named ''", surfaced through the error envelope.
If the record has no unpublished changes, the draft and the published row are identical, which is the expected result rather than an error.
Inputs: params (required). Returns: result
dataverse_retrieve_user_privilegesARead-onlyIdempotent
Retrieve all security privileges assigned to a system user via their roles.
Returns RolePrivilege objects with PrivilegeName and Depth. Use dataverse_whoami to get the caller's UserId for checking your own privileges.
Inputs: params (required). Returns: result
dataverse_validate_appARead-onlyIdempotent
Validate a model-driven app and return all errors and warnings.
Uses the ValidateApp function to check for missing required components (sitemap, etc.). An app with validation errors cannot be published. Run this before calling dataverse_publish_app to catch errors early.
Inputs: params (required). Returns: result
dataverse_validate_fetchxmlARead-onlyIdempotent
Check a FetchXML query for problems and performance warnings WITHOUT running it.
Pre-flight companion to dataverse_execute_fetchxml. Calls the unbound ValidateFetchXmlExpression function, which parses and analyses the expression server-side and reports validation errors plus performance suggestions (for example unrestricted column lists or filters that cannot use an index). No records are read and nothing is modified. Run it before executing an expensive or machine-generated query — a FetchXML query that returns results can still be a query that scans a table.
No entity set name is required: the root inside the document identifies the table.
HTTP 200 DOES NOT MEAN THE QUERY IS VALID — check has_errors / error_count. A FetchXml naming a table or attribute that does not exist comes back as a successful HTTP 200 carrying an error-severity message ("Error handling FetchXML: The entity with a name = '...' was not found in the MetadataCache"), not as an HTTP 400. Treating a non-error response as "this query works" is wrong. Read has_errors first, then errors for the error texts.
Findings are reported as: count (total messages), error_count, warning_count (count == error_count + warning_count, so nothing is dropped), has_errors, and errors (the error texts). The severity mapping is OBSERVED, NOT DOCUMENTED: live responses used 1 for performance warnings and 3 for errors, so severity
= 3 is counted as an error and < 3 as a warning. Any message whose severity is missing or not an integer is bucketed conservatively as an error rather than assumed benign, and still appears in count.
The full payload is also returned unchanged under raw_response (minus the @odata envelope): ValidationResults.Helplink, each message's LocalizedMessageText and its OptionalPropertyBag (which carries details such as AttributeCount/AttributeLimit) are worth reading. If the payload is not in the expected ValidationResults.Messages shape it is returned raw with normalized=false and no counts, rather than being guessed at.
The query is checked locally for XML well-formedness first, using a hardened parser that rejects DTDs and entity declarations, so malformed or hostile markup fails immediately with a clear message instead of costing a round trip.
Inputs: params (required). Returns: result
dataverse_validate_formxmlARead-onlyIdempotent
Validate FormXml against structural rules derived from the FormXml XSD.
Two modes: pass formxml to validate a string directly (dry-run before dataverse_set_formxml), or omit formxml to fetch and validate the live form. Returns valid=true + control list, or valid=false + full error list. All write tools run this validation automatically before every PATCH.
Inputs: params (required). Returns: result
dataverse_validate_viewARead-onlyIdempotent
Validate a Dataverse view's FetchXml and LayoutXml against 16 structural rules.
Fetches the live XML and checks FetchXml structure (rules 1-8), LayoutXml structure (rules 9-15), and column cross-reference (rule 16). Layout rules are skipped when layoutxml is null. Write tools run this automatically before every PATCH — use this for a standalone pre-check.
Inputs: params (required). Returns: result
dataverse_whoamiARead-onlyIdempotent
Return the authenticated caller's identity from the Dataverse WhoAmI endpoint.
Returns UserId, BusinessUnitId, and OrganizationId. Call at session start to confirm authentication and get the caller's UserId for privilege checks.
Inputs: params (required). Returns: result
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 97 tools
The tool set covers many distinct resources and each tool's description is extremely detailed, often cross-referencing sibling tools (e.g., dataverse_get_import_job vs dataverse_get_import_job_results). A few pairs like dataverse_get_environment_variables and dataverse_get_environment_variable_values could be confused, but the descriptions clarify the difference between definitions and values. Overall, tools are well-differentiated.
All tools follow a dataverse_ prefix with snake_case and generally verb_noun ordering (list_*, get_*, validate_*, etc.). There are minor inconsistencies like using 'get' vs 'retrieve' (get_relationship vs retrieve_unpublished) and singular/plural variance (list_solutions vs get_solution_history), but the pattern is strong and predictable.
With 97 tools, this is far beyond the typical well-scoped range. Even for a comprehensive Dataverse server, the number is extreme, and many tools are narrow inspector endpoints that could be consolidated. It exceeds the 50+ threshold that indicates extreme mismatch.
Despite the large number of tools, the server is almost entirely read-only: it contains no create/update/delete tools for records, tables, columns, relationships, choices, forms, views, web resources, or security roles. Several referenced write tools (dataverse_create_record, dataverse_update_table, etc.) are not actually present in the tool list, leaving agents unable to perform any mutation. This is a severe gap for an administration-focused server.
Maintenance
Related MCP Connectors
Query and update Teable records, manage tables, and access the Teable API with AI agents.
Web data for agents. Extract web pages, crawl websites, discover URLs, and search the web.
Connect AI agents to 1000+ apps with managed authentication and tool-calling.
Query your team's drift, vulnerability, and upgrade data from any AI assistant. OAuth 2.1, 51 tools.
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceEnables comprehensive management of Microsoft Dataverse environments, including schema operations for tables, columns, and relationships through the Dataverse Web API. It also supports solution management, security role configuration, and the generation of WebAPI calls and Mermaid ERD diagrams.MIT
- AlicenseBqualityDmaintenanceEnables AI agents to perform CRUD operations, query data, fetch schemas, and execute custom operations on Microsoft Dynamics 365 CRM entities.1024 npm2MIT
- FlicenseAqualityDmaintenanceEnables direct interaction with Microsoft Dataverse from Claude Desktop, supporting full CRUD, bulk operations, FetchXML queries, actions, and metadata discovery.20-
- AlicenseAqualityDmaintenanceProvides intelligent access to PowerPlatform/Dataverse entities and records, enabling entity metadata exploration, OData querying, relationship mapping, and AI-assisted query building through MCP.893 npmMIT