Dataverse MCP Server
Server Configuration
Describes the environment variables required to run the server.
| Name | Required | Description | Default |
|---|---|---|---|
| DATAVERSE_URL | No | Fallback org URL used when a tool call omits dataverse_url | |
| DATAVERSE_AUTH_TYPE | No | Authentication method: azure_cli or interactive | azure_cli |
| DATAVERSE_ALLOW_WRITE | No | Set to true to register create, update, associate, merge, and schema mutation tools | false |
| DATAVERSE_ALLOW_DELETE | No | Set to true to register delete and disassociate tools | false |
Instructions
Guidance the server publishes about itself, which clients place ahead of the tool catalog so the model reads it before choosing anything.
This server publishes no instructions, or was last inspected before Glama recorded them.
Capabilities
Features and capabilities supported by this server
Protocol revision2025-11-25
| Capability | Details |
|---|---|
| tools | {
"listChanged": false
} |
| prompts | {
"listChanged": false
} |
| resources | {
"subscribe": false,
"listChanged": false
} |
| experimental | {} |
Tools
Functions exposed to the LLM to take actions
| Name | Description |
|---|---|
| dataverse_list_environmentsA | 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. |
| dataverse_whoamiA | 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. |
| dataverse_get_organization_infoA | 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. |
| dataverse_get_entity_setsA | 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. |
| dataverse_retrieve_user_privilegesA | 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. |
| dataverse_retrieve_principal_accessA | 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. |
| dataverse_get_settingA | 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. |
| dataverse_list_solutionsA | 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. |
| dataverse_get_solutionA | 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. |
| dataverse_list_solution_componentsA | 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). |
| dataverse_get_solution_historyA | 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:
|
| dataverse_list_solution_historiesA | 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:
Use msdyn_suboperation to determine whether a history record represents a standard update or a true upgrade with component deletion. |
| dataverse_get_cloud_flowsA | 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. |
| dataverse_list_processesA | 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'"). |
| dataverse_export_solutionA | 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. |
| dataverse_get_import_jobA | 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. |
| dataverse_list_import_jobsA | 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). |
| dataverse_get_import_job_resultsA | 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. |
| dataverse_query_tableA | 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. |
| dataverse_execute_fetchxmlA | 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. |
| dataverse_validate_fetchxmlA | 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
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. |
| dataverse_get_recordA | 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. |
| dataverse_count_recordsA | 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. |
| dataverse_get_total_record_countsA | 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. |
| dataverse_aggregate_tableA | 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. |
| 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}]. |
| dataverse_list_tablesA | 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. |
| dataverse_get_table_metadataA | 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. |
| dataverse_list_columnsA | 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. |
| dataverse_get_columnA | 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. |
| dataverse_list_choice_column_optionsA | 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. |
| dataverse_list_relationshipsA | 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. |
| dataverse_get_relationshipA | 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. |
| dataverse_list_choicesA | 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. |
| dataverse_get_choiceA | 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. |
| dataverse_check_relationship_eligibilityA | 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. |
| dataverse_list_alternate_keysA | 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. |
| dataverse_is_component_customizableA | 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. |
| dataverse_list_languagesA | 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:
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. |
| dataverse_get_valid_relationship_entitiesA | 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:
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. |
| dataverse_list_formsA | 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. |
| dataverse_get_formA | 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. |
| dataverse_validate_formxmlA | 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. |
| dataverse_list_viewsA | 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. |
| dataverse_get_viewA | 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. |
| dataverse_validate_viewA | 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. |
| dataverse_list_appsA | 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. |
| dataverse_get_appA | 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. |
| dataverse_validate_appA | 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. |
| dataverse_list_plugin_type_statisticsA | 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. |
| dataverse_get_plugin_trace_log_settingA | 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. |
| dataverse_list_plugin_trace_logsA | 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:
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. |
| dataverse_list_connection_referencesA | 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. |
| dataverse_get_connection_referenceA | 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. |
| dataverse_get_plugin_assemblyA | 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. |
| dataverse_list_plugin_assembliesA | 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. |
| dataverse_get_plugin_packageA | 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. |
| dataverse_list_plugin_packagesA | 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. |
| dataverse_get_plugin_typeA | 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. |
| dataverse_list_plugin_typesA | 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. |
| dataverse_get_sdk_messageA | 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. |
| dataverse_list_sdk_messagesA | 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. |
| dataverse_get_sdk_message_filterA | 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. |
| dataverse_list_sdk_message_filtersA | 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. |
| dataverse_get_plugin_stepA | Retrieve a single SDK message processing step record by its GUID. Steps are the third node in the prerequisite chain: assembly → type → step → image. |
| dataverse_list_plugin_stepsA | 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. |
| dataverse_get_plugin_step_imageA | 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. |
| dataverse_list_plugin_step_imagesA | List plug-in step images (pre/post entity snapshots) registered against steps. Use step_id to scope to one processing step. |
| dataverse_get_environment_variablesA | 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. |
| dataverse_get_environment_variable_valuesA | Get environment variable value record(s). Provide exactly one targeting path:
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. |
| dataverse_list_security_rolesA | 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. |
| dataverse_get_security_roleA | Retrieve a single Dataverse security role by its GUID. Returns full role details including name, business unit, and managed status. |
| dataverse_get_role_privilegesA | 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:
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. |
| dataverse_get_team_privilegesA | 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. |
| dataverse_list_privilegesA | 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. |
| dataverse_retrieve_access_originA | 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:
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:
Other live-confirmed behaviour:
|
| dataverse_list_shared_principalsA | Answer "WHO has this record because it was SHARED with them?". Merges two unbound Web API functions that answer one question:
This is the list neither neighbouring tool can produce. dataverse_retrieve_principal_access answers "which rights does ONE named principal have" (the mask), dataverse_retrieve_access_origin answers "WHY does ONE named principal have them" — both need you to already know who to ask about. This one enumerates them. MIND THE INPUT ASYMMETRY. This tool takes the PLURAL entity_set_name ('accounts'), because the target is expressed as an OData EntityReference and an @odata.id names a collection. dataverse_retrieve_access_origin takes the SINGULAR logical_name ('account'). Confusing the two is the easy caller error here — use dataverse_get_entity_sets to confirm the plural, which is irregular often enough ('webresourceset') that guessing costs a 404. A WRONG ENTITY SET NAME LOOKS EXACTLY LIKE A MISSING RECORD. VERIFIED LIVE: a nonexistent record id and a VALID id paired with the WRONG entity set both return HTTP 404 [0x80040217] "Entity '' With Id = Does Not Exist" from BOTH functions — the same status, the same error code, indistinguishable text. Both calls therefore fail and you get the standard {"error": true, "message": ...} envelope. So when this tool errors with "Does Not Exist", CHECK THE ENTITY SET NAME FIRST (plural — 'accounts', not 'account'; see the asymmetry note above) before concluding the record is gone. That singular-for-plural slip is the likeliest cause and it misdiagnoses as a missing record. THE TWO CALLS FAIL INDEPENDENTLY. Each is made on its own: if one is unavailable or privilege-gated, its failure is reported in partial_errors and the other's data is still returned. Only a failure of BOTH yields the standard {"error": true, "message": ...} envelope. RetrieveSharedLinks is in principle the more likely of the two to be missing (it was available on the org tested, never landing in partial_errors), so a partial_errors entry naming it is an expected outcome rather than an error — check partial_errors before concluding a record is unshared. RESPONSE SHAPES (verified live). Microsoft Learn documents RetrieveSharedPrincipalsAndAccessResponse but not its inner properties; live runs confirm the collection arrives under PrincipalAccesses, and that is the name tried first before the by-shape fallback. RetrieveSharedLinks returns Collection(team), an ordinary OData collection, and its entries duly arrive under the standard 'value' property. Each block reports the source it was found under — check it. If a payload cannot be identified unambiguously that block carries normalized: false, no counts, and the raw payload (minus the @odata.* envelope) under raw_response; nothing is fabricated. Both lists are trimmed to top (default 50, max 1000) because neither function pages server-side. count, total_count and has_more are reported per block and total_count is always the full number Dataverse returned. An empty result is NOT proof the record is private: these functions report explicit shares (the POA table) that the CALLER can see, not access granted by ownership, security roles, team membership or the business-unit hierarchy. Use dataverse_retrieve_access_origin for a specific principal, and read partial_errors before drawing any conclusion. |
| dataverse_list_teamsA | 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. |
| dataverse_get_teamA | Retrieve a single Dataverse team by its GUID. Returns full team details including name, type, and business unit. |
| dataverse_list_usersA | 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. |
| dataverse_get_userA | 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. |
| dataverse_list_business_unitsA | 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"). |
| dataverse_audit_user_accessA | 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. |
| dataverse_retrieve_record_change_historyA | 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:
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':'()'} |
| dataverse_get_audit_detailsA | 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):
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()/Microsoft.Dynamics.CRM.RetrieveAuditDetails |
| dataverse_list_auditA | Query the audit table with optional OData filters. Returns audit records from the 'audits' entity set. Common columns:
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. |
| dataverse_get_attribute_change_historyA | 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:
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:
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:
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' |
| dataverse_list_async_operationsA | 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. |
| dataverse_get_async_operationA | 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. |
| dataverse_list_web_resourcesA | 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. |
| dataverse_get_web_resourceA | 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.) |
| dataverse_list_custom_apisA | 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. |
| dataverse_get_custom_apiA | Retrieve a single Custom API record by its GUID, including expanded request parameters and response properties. Returns all default columns plus:
|
| dataverse_list_custom_api_request_parametersA | 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. |
| dataverse_list_custom_api_response_propertiesA | 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. |
| dataverse_analyze_dependenciesA | Analyze dependencies for a Dataverse solution component. Exposes three directions via the direction parameter:
Use component_type integer codes (1=Entity, 2=Attribute, 61=WebResource, etc.) and the component's metadata GUID for component_id. |
| dataverse_retrieve_unpublishedA | 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.
If the record has no unpublished changes, the draft and the published row are identical, which is the expected result rather than an error. |
Prompts
Interactive templates invoked by user choice
| Name | Description |
|---|---|
No prompts | |
Resources
Contextual data attached and managed by the client
| Name | Description |
|---|---|
No resources | |
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/ryanmichaeljames/dataverse-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server