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.
Table of contents
- Why record-level tools aren’t enough for business operations
- How the agent finds and calls a custom API
- Anatomy of an agent-ready custom API
- One transaction per business operation
- Design decisions you can’t undo
- Setting it up end to end
- What doesn’t work yet, and what we don’t know
- Billing and licensing
- Security and governance
- What this changes for architects
- Reference: custom API properties for agent use
- Resources

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.

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.
Step 1: search
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.

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 description | Agent-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.

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.

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.
| Property | Options | Can change later? | My recommendation for agent use |
| Unique name | Publisher prefix + name | No | Verb-first business outcome, such as CreateCaseWithFollowUp |
| Is Function | Function (GET) or action (POST) | No | Action for anything that writes; function only for read-only lookups |
| Binding type | Global, Entity, EntityCollection | No | Global, with the record ID as a parameter [VERIFY: bound APIs in invoke_api] |
| Allowed custom processing step type | None, Async Only, Sync and Async | No | None or Async Only, so other plug-ins can’t change the contract |
| Is Optional (per parameter) | Required or optional | No | Required for anything the operation can’t run without |
| Parameter type | 13 types | No | String with documented values, until richer types are confirmed |
| Is Private | Yes or No | Yes | See the security section |
| Execute Privilege Name | An existing privilege | Yes | Set 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.

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
- Create or pick a solution with a publisher whose prefix you want in the unique name.
- 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.
- Create the request parameters. Select New > More > Other > Custom API Request Parameter for each input. Give each one a description written for the model.
- Create the response properties the same way, with Custom API Response Property.
- 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.
- 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_apigenerally 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
searchreturn custom APIs with Is Private set to Yes? - Which APIs appear. Does
searchalso 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
| Property | What the agent sees | Recommendation |
| Unique name | The name in describe and invoke_api | Verb-first, business outcome, publisher prefix |
| Display name | The label in search results | Readable outcome, such as “Create Case With Follow-Up” |
| Description | When to use the API | State the outcome and that it runs as one transaction |
| Request parameter description | What to put in each input | List allowed values and formats |
| Is Optional | The required flag in describe | Required unless the operation has a safe default |
| Response properties | The result of invoke_api | IDs of created records plus a one-sentence Message |
| Execute Privilege Name | Nothing directly; the call fails without it | Set it for every API that writes |
| Is Private | Unknown in MCP [VERIFY] | Don’t rely on it for security |
| Custom processing step type | Nothing directly | None 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
- Custom APIs just landed in the Dataverse MCP Server, Agents just got smarter — Microsoft Power Platform blog, 26 October 2026
- Dataverse MCP Server: understanding the new tool shape — Microsoft Power Platform blog, 8 June 2026
- Create and use custom APIs — Microsoft Learn
- Create a custom API in Power Apps — Microsoft Learn
- CustomAPI tables — Microsoft Learn
- Connect to Dataverse with Model Context Protocol — Microsoft Learn
- Connect to Dataverse with model context protocol FAQ — Microsoft Learn
- Configure the Dataverse MCP server for an environment — Microsoft Learn
- Use preview tools and upcoming features in Dataverse MCP server — Microsoft Learn
- Connect Dataverse MCP with GitHub Copilot in Visual Studio Code and Copilot CLI — Microsoft Learn
- Analyze plug-in performance — Microsoft Learn
- Troubleshoot Dataverse plug-ins — Microsoft Learn
- Microsoft MCP management MCP server reference (preview) — Microsoft Learn
- Dataverse MCP Server: The 15 Tools That Make Power Platform Agent-Ready — aidevme.com
- Dataverse Skills for Enterprise Architects: Governance, ALM, MCP Billing, and Production Readiness — aidevme.com

Leave a Reply