Custom APIs in the Dataverse MCP Server: Your Business Logic Becomes the Agent Contract

An AI agent hands Dataverse one sealed operation holding a case, a note and a task instead of three loose calls

Estimated reading time: 17 minutes

A support agent in GitHub Copilot gets a simple request. Create a high-priority case for a customer whose shipment arrived damaged, add a note, and schedule a follow-up call for Friday. With record-level tools, the agent makes three calls: create the case, create the note, create the task. Now imagine the third call fails. The case and the note already exist, but nobody owns the follow-up. Who cleans up?

That’s the problem Microsoft’s 26 October announcement addresses. The Dataverse MCP Server can now find and call custom APIs. As a result, the agent no longer assembles your business process from table operations. Instead, it calls the operation you designed, and Dataverse runs it as one unit.

This post explains how the agent finds and calls a custom API. Then it shows how to design one that an agent picks correctly, and covers the decisions you can’t undo later. Finally, it looks at billing, security and what changes for architects. I based it on Microsoft’s announcement and the Microsoft Learn documentation as of 28 October 2026.

An agent juggles three separate record cards over three tables while one falls, leaving half-finished data

Why record-level tools aren’t enough for business operations

Record-level tools give an agent the table surface, not your process. The Dataverse MCP Server’s generally available tools include create_record, update_record, read_query and describe. They’re the 15 tools I covered in Dataverse MCP Server: The 15 Tools That Make Power Platform Agent-Ready. They work well for exploring data and making single changes.

A business operation is different, though. Creating a case with a note and a follow-up task touches three tables: incident, annotation and task. To do it with record tools, the agent has to know all three schemas. It also has to know which lookup links them and in which order to write them. Above all, it has to handle failure halfway through.

Kent Weare’s announcement names three costs of that approach:

  • Partial failures. Separate calls can leave half-finished data behind.
  • Compensation logic. Someone has to write the code that undoes the first two writes.
  • A low-level surface. The agent sees raw tables instead of the operation your business actually performs.

A custom API removes all three. You already use custom APIs to package server-side logic for apps, flows and integrations. Now the same operation becomes a tool an agent can call.


An agent finds a sealed operation on a shelf, reads its blank spec card, then triggers it, in three steps

How the agent finds and calls a custom API

The agent uses three tool calls: search, describe and invoke_api. None of them needs a hand-written MCP wrapper. Instead, the server reads your custom API’s metadata and presents it to the model.

Sequence diagram of an agent calling a Dataverse custom API through the Dataverse MCP Server: the user asks for a high-priority case, a note and a Friday follow-up; the agent calls search, then describe, then invoke_api; the server runs the custom API's plug-in as the calling user, which creates an incident, an annotation and a task in one Dataverse transaction. If every step succeeds, the IDs and a message return to the agent and the user; if a step fails, the plug-in throws InvalidPluginExecutionException, everything is rolled back and the agent explains the error.
How an agent calls a Dataverse custom API through the MCP server

The agent calls search with keywords from the user’s request. According to the announcement, the results list custom APIs with a path and the display name, for example api/<prefix>_CreateCaseWithFollowUp — Create Case With Follow-Up. Importantly, the search tool’s own instructions steer the model. They tell it to “first check whether a suitable Custom API exists here” and to “prefer invoking it over doing the work yourself.”

That instruction matters more than it looks. Without it, a model would happily chain three create_record calls. With it, the model looks for your operation first.

Step 2: describe

Next, the agent calls describe with the path from the search result. The response contains the API’s unique name, its description and the request parameters. Each parameter has a name, a type, a required flag and a description. All of it comes from the custom API’s metadata in Dataverse. Here is the shortened response from the announcement:

{
  "apiUniqueName": "aidevme_CreateCaseWithFollowUp",
  "description": "Atomically creates a support case, attached note, and follow-up task for a damaged-shipment report.",
  "inputs": [
    { "name": "AccountId", "type": "string", "required": true, "description": "Account reporting the damaged shipment." },
    { "name": "Priority", "type": "string", "required": true, "description": "Case priority: High, Normal, or Low." },
    { "name": "FollowUpDueDate", "type": "string", "required": true, "description": "Due date and time for the follow-up task." }
  ]
}

Step 3: invoke_api

Finally, the agent calls invoke_api with two properties. apiUniqueName names the API, and apiInputs is a JSON object with one property per request parameter:

{
  "apiUniqueName": "aidevme_CreateCaseWithFollowUp",
  "apiInputs": {
    "AccountId": "00000000-0000-0000-0000-000000000000",
    "CaseTitle": "Damaged shipment",
    "CaseDescription": "Customer reports the shipment arrived damaged.",
    "Priority": "High",
    "CaseOrigin": "Web",
    "FollowUpSubject": "Call customer about damaged shipment",
    "FollowUpDueDate": "2026-10-30T14:00:00Z"
  }
}

In this example, the response returned the case ID, the ticket number, the note ID, the task ID and a readable message. Consequently, the agent can confirm the result to the user in one sentence.


Exploded view of a custom API capsule with a blank label, shaped input slots and an output tray the agent reads

Anatomy of an agent-ready custom API

An agent-ready custom API is an ordinary custom API with unusually good metadata. The model never sees your C# code. It sees names, descriptions and types. Therefore, those fields decide whether the agent picks your API and fills it in correctly.

The unique name and the publisher prefix

Every custom API’s unique name must start with the customization prefix of your solution publisher. It can’t contain special characters, and you can’t change it after you save. The unique name is also the message name that apps and flows call.

Choose a verb-first name that describes the business outcome, such as CreateCaseWithFollowUp. A name like ProcessCase2 tells the model nothing. The display name appears in search results too, so give it the same care.

Descriptions are the prompt

The custom API table has a Description column, and so do request parameters and response properties. Learn describes these as text for tooltips in apps. For an agent, however, they’re the prompt. They tell the model when to call the API and what each value means.

Weak descriptionAgent-ready description
API“Creates a case”“Atomically creates a support case, attached note, and follow-up task for a damaged-shipment report.”
Parameter“Priority”“Case priority: High, Normal, or Low.”
Parameter“Due date”“Due date and time for the follow-up task, in ISO 8601 UTC.”
Response“Message”“One sentence to show the user, including the case ticket number.”

The good examples share a pattern. They state the outcome, list the allowed values and name the format. Write them as you would write instructions for a new colleague.

Request parameters

A request parameter has a unique name, a type and an IsOptional flag. The types include String, Integer, Decimal, Float, Boolean, DateTime, Guid, Picklist, Money, EntityReference, Entity, EntityCollection and StringArray.

Microsoft’s example uses String for every input, including the priority and the due date. The descriptions then list the allowed values. That approach keeps the JSON simple for the model. On the other hand, your plug-in must validate every value itself. I recommend that trade-off for now, because the announcement doesn’t say how invoke_api maps richer types. [VERIFY: how invoke_api passes Picklist, Money, EntityReference and Entity inputs]

Keep the parameter list short. Every required parameter is something the agent has to find or ask for. If the agent can’t fill a value with confidence, it should ask the user rather than guess.

Response properties

An action doesn’t need response properties, but an agent does. Return the IDs of the records you created, so the agent can link to them or continue working. In addition, return a short human-readable message. Microsoft’s example returns CaseId, TicketNumber, NoteId, TaskId and Message. That combination lets the agent confirm the result without another query.


Three linked records sealed in one vault with a rollback arrow, showing they succeed or fail together

One transaction per business operation

The main reason to use a custom API is the transaction. According to the announcement, the main operation runs in a single Dataverse transaction. If any step fails, the plug-in throws InvalidPluginExecutionException, and Dataverse rolls back everything. So the case, the note and the task either all exist or none of them do.

The plug-in that implements the main operation is an ordinary plug-in. You register the assembly, but you don’t register a step. Then you set the custom API’s Plugin Type to your class. Inputs arrive in InputParameters, and you set outputs in OutputParameters.

Here is a skeleton for the example API. It shows the shape, not production code.

using System;
using Microsoft.Xrm.Sdk;

public class CreateCaseWithFollowUp : IPlugin
{
    public void Execute(IServiceProvider serviceProvider)
    {
        var context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));
        var factory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));
        var service = factory.CreateOrganizationService(context.UserId);

        if (!Guid.TryParse((string)context.InputParameters["AccountId"], out var accountId))
        {
            throw new InvalidPluginExecutionException("AccountId must be the GUID of an existing account.");
        }

        var caseRecord = new Entity("incident")
        {
            ["title"] = (string)context.InputParameters["CaseTitle"],
            ["description"] = (string)context.InputParameters["CaseDescription"],
            ["customerid"] = new EntityReference("account", accountId),
            ["prioritycode"] = new OptionSetValue(MapPriority((string)context.InputParameters["Priority"])),
            ["caseorigincode"] = new OptionSetValue(MapOrigin((string)context.InputParameters["CaseOrigin"]))
        };
        var caseId = service.Create(caseRecord);

        var noteId = service.Create(new Entity("annotation")
        {
            ["subject"] = "Damaged shipment report",
            ["notetext"] = (string)context.InputParameters["CaseDescription"],
            ["objectid"] = new EntityReference("incident", caseId)
        });

        var taskId = service.Create(new Entity("task")
        {
            ["subject"] = (string)context.InputParameters["FollowUpSubject"],
            ["scheduledend"] = DateTime.Parse((string)context.InputParameters["FollowUpDueDate"]).ToUniversalTime(),
            ["regardingobjectid"] = new EntityReference("incident", caseId)
        });

        var ticket = service.Retrieve("incident", caseId, new Microsoft.Xrm.Sdk.Query.ColumnSet("ticketnumber"))
            .GetAttributeValue<string>("ticketnumber");

        context.OutputParameters["CaseId"] = caseId.ToString();
        context.OutputParameters["TicketNumber"] = ticket;
        context.OutputParameters["NoteId"] = noteId.ToString();
        context.OutputParameters["TaskId"] = taskId.ToString();
        context.OutputParameters["Message"] = $"Case {ticket} was created with a follow-up task.";
    }

    // Map the agent's string values to your environment's choice values; check them in your org.
    private static int MapPriority(string value) => throw new NotImplementedException();
    private static int MapOrigin(string value) => throw new NotImplementedException();
}

Three details matter in this code. First, it runs as the calling user (context.UserId), so Dataverse security still applies. Second, it validates inputs and throws a readable InvalidPluginExecutionException. The agent can relay that message to the user. Third, it never catches and swallows a failed service call. Learn’s troubleshooting guide explains why: in a synchronous plug-in, any failed data operation rolls back the whole transaction anyway.

Keep the operation short. A Dataverse message operation must finish within 2 minutes, including all synchronous plug-ins. Otherwise, Dataverse throws a timeout and rolls back. Learn recommends keeping a single plug-in under 2 seconds. For long-running work, Learn points to Power Automate, Logic Apps or other Azure services instead, not to a custom API an agent waits on.


An architect sets dials and switches on a blueprint before a seal locks them in place for good

Design decisions you can’t undo

Several custom API properties can’t change after you save the record. If you get them wrong, you delete the custom API and create it again. That breaks every caller, so decide before you build.

PropertyOptionsCan change later?My recommendation for agent use
Unique namePublisher prefix + nameNoVerb-first business outcome, such as CreateCaseWithFollowUp
Is FunctionFunction (GET) or action (POST)NoAction for anything that writes; function only for read-only lookups
Binding typeGlobal, Entity, EntityCollectionNoGlobal, with the record ID as a parameter [VERIFY: bound APIs in invoke_api]
Allowed custom processing step typeNone, Async Only, Sync and AsyncNoNone or Async Only, so other plug-ins can’t change the contract
Is Optional (per parameter)Required or optionalNoRequired for anything the operation can’t run without
Parameter type13 typesNoString with documented values, until richer types are confirmed
Is PrivateYes or NoYesSee the security section
Execute Privilege NameAn existing privilegeYesSet one for any API that writes

Two of these deserve a closer look.

Function or action

A function is called with HTTP GET and passes all parameters in the URL, which has a 32 KB limit. Functions must also return at least one response property. An action uses POST and passes parameters in the body. Learn adds that the Dataverse connector only supports actions in Power Automate. For an operation that writes data, an action is the right choice. [VERIFY: whether invoke_api supports custom API functions]

Global or bound

Binding is an OData concept. An Entity binding adds a Target parameter of type EntityReference automatically. An EntityCollection binding attaches the operation to a table’s entity set. However, Learn notes that you can build every custom API as Global and pass the record ID as a normal parameter. Microsoft’s MCP example is unbound. Until bound APIs are confirmed for invoke_api, I’d keep agent-facing APIs Global.


One custom API capsule at the center, connected to an app, a flow, a script and an agent

Setting it up end to end

You create the custom API once, and every channel can use it: apps, flows, code and now agents. Learn documents several ways to create one: the Plug-in Registration tool’s designer, Power Apps, code and solution files. Here is the order of work in Power Apps.

Build the custom API

  1. Create or pick a solution with a publisher whose prefix you want in the unique name.
  2. Create the custom API record. In the solution, select New > More > Other > Custom API. Set the unique name, display name, description, binding type, Is Function and the custom processing step type.
  3. Create the request parameters. Select New > More > Other > Custom API Request Parameter for each input. Give each one a description written for the model.
  4. Create the response properties the same way, with Custom API Response Property.
  5. Test without a plug-in. You can call the API before any code exists. The response properties then return their default values, which confirms the contract.
  6. Write and register the plug-in assembly. Then set the custom API’s Plugin Type lookup to your class. Add the assembly and its types to the solution.

Connect an agent

1. Allow your MCP client in the Power Platform admin center. Go to the environment’s Settings > ProductFeatures, check Allow MCP clients to interact with Dataverse MCP server, and enable the client under Advanced Settings.

2. Connect the client. For GitHub Copilot CLI, Learn shows this configuration in ~/.copilot/mcp-config.json:

{
  "mcpServers": {
    "DataverseMcp": {
      "type": "http",
      "url": "https://yourorg.crm.dynamics.com/api/mcp"
    }
  }
}

3. Test discovery. Ask the agent for the business outcome in plain words and check that it calls search, then describe, then invoke_api.

Learn’s list of generally available tools doesn’t include invoke_api yet. If your client doesn’t see it, try the preview endpoint. An admin enables it with Allow MCP clients to interact with Dataverse MCP server (Preview version).


What doesn’t work yet, and what we don’t know

One limit is confirmed: custom process actions aren’t supported in the Dataverse MCP Server. These are the older no-code actions built in the process designer. If your business logic lives in one, you’ll need to rebuild it as a custom API first. The community’s Custom Action to Custom API Converter for XrmToolBox can help, but Microsoft doesn’t support community tools.

Beyond that, the announcement leaves several questions open. I’ll update this post after testing them.

  • Availability. Is invoke_api generally available on /api/mcp, or a preview tool on /api/mcp_preview?
  • Bound APIs and functions. The example is an unbound action. How do Entity and EntityCollection bindings and functions work?
  • Private APIs. Does search return custom APIs with Is Private set to Yes?
  • Which APIs appear. Does search also return Microsoft’s own custom APIs and those of installed apps?
  • Privileges. Does Execute Privilege Name hide an API from search, or only block the call?
  • Parameter types. How are Picklist, Money, EntityReference and Entity inputs passed?

Billing and licensing

Dataverse MCP tools have been billed since 15 December 2025 when agents built outside Copilot Studio use them. According to Learn, search_data is billed at the same Copilot credit rate as Tenant graph grounding. All other tools, including the metadata search tool, use the “Text and generative AI tools (basic) per 10 responses” rate.

There is an exception. If you have qualifying Dynamics 365 Premium licenses or a Microsoft 365 Copilot User Subscription License, you aren’t charged for accessing Dynamics 365 data, even from outside Copilot Studio.


Security and governance

A custom API turns an agent’s request into code that runs on your server. That’s powerful, so the governance questions shift from “which tables can the agent touch” to “which operations can it run”.

The agent runs as the user

The Dataverse MCP Server respects security roles and row-level security. Users can only reach tables and records their role permits, and no extra MCP-specific controls exist. A custom API plug-in that uses context.UserId keeps that promise. A plug-in that runs as SYSTEM or impersonates another user doesn’t. Review every agent-facing API for that before you expose it.

To restrict who can call an API at all, set Execute Privilege Name. You can’t create a new privilege for it yet. Learn’s workaround is to create a custom table and use one of its generated privileges, such as prvCreatenew_myaction. Then grant that privilege only to the roles that may run the operation.

Write-capable tools need a decision

Microsoft’s June post on the new tool shape gives admins a clear rule: “Treat write-capable tools differently from read-only tools.” invoke_api can do anything your custom APIs do, so treat it as a write tool. If your MCP client supports tool allow lists or approval prompts, decide where invoke_api belongs. If you maintain allow lists by tool name, add invoke_api deliberately rather than by accident.

Discovery is an attack surface

Every custom API the agent can find is a tool it can call. Is Private doesn’t help here as a security control. It hides an API from the $metadata document, but Learn is clear that callers who know the name can still use it. Real control comes from security roles and Execute Privilege Name.

Also note what the client allow list covers. The admin setting for MCP clients applies to the /api/mcp entry point. A custom API is a regular Dataverse API, so apps and scripts with the right privileges can call it through the Web API anyway. MCP is one more door to the same room.

ALM: the description is now a contract

Ship custom APIs in a managed solution and set Is Customizable to false on the API, its parameters and its response properties. Otherwise, someone can change the definition and break your callers. Treat the descriptions as part of the contract, too. A changed description changes how the model behaves, so review it like code.


What this changes for architects

My view: custom APIs are becoming the boundary you design for agents. Until now, an agent on Dataverse saw tables. With this change, you can give it business operations instead, with validation, transactions and security in one place.

That shifts some design work. First, inventory the operations agents should run. Next, decide which ones deserve a custom API and which are fine as record calls. Then write the descriptions with the same care as an API contract for a partner team. The model is now one of your API’s consumers.

It’s worth separating this from the Microsoft MCP management server. That server’s CreateToolWithCustomAPI tool adds a custom API as a named tool to a custom MCP server you build, with an optional tool name and description. By contrast, the Dataverse MCP Server feature needs no setup: the agent finds your custom APIs through search. Use the management server when you want a curated, smaller tool set for one agent.

Finally, look at your custom process actions. They don’t work through the MCP server, and Learn already positions custom APIs as the option built for developers who define logic in code. This announcement gives you one more reason to migrate.


Reference: custom API properties for agent use

PropertyWhat the agent seesRecommendation
Unique nameThe name in describe and invoke_apiVerb-first, business outcome, publisher prefix
Display nameThe label in search resultsReadable outcome, such as “Create Case With Follow-Up”
DescriptionWhen to use the APIState the outcome and that it runs as one transaction
Request parameter descriptionWhat to put in each inputList allowed values and formats
Is OptionalThe required flag in describeRequired unless the operation has a safe default
Response propertiesThe result of invoke_apiIDs of created records plus a one-sentence Message
Execute Privilege NameNothing directly; the call fails without itSet it for every API that writes
Is PrivateUnknown in MCP [VERIFY]Don’t rely on it for security
Custom processing step typeNothing directlyNone or Async Only for a stable contract

Custom APIs used to be a developer’s tool for apps and integrations. Now they’re also the most precise way to tell an agent what your business allows.

Over to you: which operation in your environment would you expose to an agent first, and which would you never expose?


Resources

Leave a Reply