MCP Server

The Power Portals Pro MCP server gives AI coding assistants first-hand knowledge of this framework while you build. It is a Model Context Protocol server, it ships with the framework, and there is no licence key and nothing to sign up for.

Why it exists

Two things an agent cannot get from documentation alone, and both of them cause broken code:

What it answers

Note

Every Dataverse tool reads metadata. None of them reads, writes or deletes a record, so pointing an agent at a production environment lets it learn the shape of your data without being able to touch it.

Adding it to a project

Two places it can live

There is a chicken-and-egg here worth being explicit about. The templates write the configuration into the project they create — but the server is at its most useful before that project exists, when you are choosing a template and its options. So install it once for yourself, and let the per-project file take over from there.

Once per machine, for any folder

This makes the server available everywhere, including an empty folder you are about to scaffold into. It can then answer which template to use, what the options mean, and what to configure afterwards. Deliberately unpinned, so it tracks the latest release:

Visual Studio reads a user-level .mcp.json from your profile folder, Cursor uses ~/.cursor/mcp.json, and VS Code has a user-level MCP configuration in its settings. Each takes the same servers block.

Per project, pinned to its version

Projects created from the Power Portals Pro templates are wired up already: the template writes a .mcp.json at the solution root, pinned to the matching server version. There is nothing to do.

To add it to an existing project, run this in the project folder — it writes that same .mcp.json for you:

Note what that does not do: it leaves the server unpinned, so the folder gets the newest published server rather than the one matching the PowerPortalsPro.* packages your project references. That is the right default for a project tracking the current release, and the wrong one for a project deliberately held on an older version — pin it by appending the version, the way the templates do:

Or write the file yourself, at the root of your solution:

Which file your editor reads

Visual Studio 2022 17.14+ and Visual Studio 2026 read .mcp.json from the solution folder, and so does Claude Code. VS Code reads .vscode/mcp.json and Cursor reads .cursor/mcp.json — both use the same servers block, so copy it across.

dnx ships with the .NET 10 SDK, which you already have: every Power Portals Pro project is hosted by ASP.NET Core, React SPAs included. The first run downloads the package and caches it.

Having both is the intended setup. The two entries use the same server name, so a client that understands both scopes uses the project's — which means you get the version-matched answers inside a project, and the latest ones anywhere else.

The ppp-mcp command, if you want it

The terminal commands on this page — login, status, logout and the rest — run through dnx with nothing installed: dnx PowerPortalsPro.Mcp login. If you would rather type ppp-mcp login, install the package as a .NET global tool:

That puts ppp-mcp on your PATH: the .NET SDK normally adds its global tools folder — %USERPROFILE%\.dotnet\tools on Windows, ~/.dotnet/tools on macOS and Linux — to it. If the command is not found, open a new terminal; if it still is not, add that folder to PATH yourself.

A global tool stays on the version you installed, where dnx fetches the latest, so update it as described below. Your MCP client can keep launching the server through dnx either way: both read the same folder bindings and the same saved sign-ins.

Checking it is working

Ask your assistant something only the server can answer — for example “use get_component_api to show me every parameter on MainGrid”. If the tool is not available, the client has not picked up the configuration file; most need a restart after it is added.

Updating

How you update depends on how you installed it.

Installed as a global tool (dotnet tool install --global PowerPortalsPro.Mcp, which puts ppp-mcp on your PATH):

Important

Close your MCP client first. A running server holds its own files open, and the update fails with Access to the path … is denied until nothing is using them. Start the client again afterwards to load the new version.

Connecting to Dataverse

The documentation tools need no configuration at all. The metadata tools need a connection to your environment, and that connection belongs to the folder, not to the machine. Run this once in the project:

Environments belong to the folder

That binds this folder to an environment. If you work across several customers, every project stays signed in to its own environments and agents in different folders never cross over. This is the one thing pac auth select cannot do — it switches a single machine-wide profile, so changing customer in one place changes it everywhere.

Sign-in uses the device-code flow by default, so it works over SSH, inside a container and in WSL, and you can finish it on a phone. Pass --browser if you would rather open one.

Several environments in one folder

One folder can be signed in to several environments at once — a customer's development, test and production environments, say. Each login with another environment adds it to the folder's list, and --name gives it the name you will refer to it by:

The first environment is the primary, and every tool acts on it unless you ask for another: “list the views on contact in test” is answered from test, and the next question goes back to the primary. Pass --make-primary to login to add an environment as the primary instead, or ask your assistant to reorder them.

Every answer that touched Dataverse starts by naming the environment it came from — its name, its position and its URL — so a change made in production never reads the same as one made in development.

Where things are stored

The token cache is keyed by environment, so a second clone of the same customer's repository does not ask you to sign in again, while a different customer always does — and so does each environment you add to a folder.

Commands

Run them as dnx PowerPortalsPro.Mcp <command>, or as ppp-mcp <command> if you installed the global tool. All of these act on the current folder unless you pass --workspace.

Other ways to connect

The environment and credentials are resolved in this order, so the most explicit statement of intent wins:

  1. Environment variables — PPP_D365_URL, PPP_D365_CLIENT_ID and PPP_D365_CLIENT_SECRET in the server's env block. A service principal, and the way to wire this up in CI or a shared container. PPP_D365_URL becomes the primary environment, ahead of any the folder is bound to, and the service principal is used for the primary only.
  2. The folder's sign-in — written by login. The normal path.
  3. The project's own settings — the same D365:Url, D365:ClientId and D365:ClientSecret (or D365:Secret) the portal itself reads from user secrets and appsettings. A configured Power Portals Pro project therefore often needs no sign-in at all.

Power Platform CLI profiles

If you already keep environments in pac, you can start from one rather than hunting for a URL:

The CLI has no command that hands out an access token, and it keeps credentials in its own store, so its session cannot be reused directly. What --pac-profile does is take the environment and the account from a profile and sign in as that account — usually without prompting, because you already have a session.

Note

The CLI's own active profile is never changed. pac auth select is deliberately not used: it would move every project at once.

The tools

You rarely need to name a tool — assistants pick them from the descriptions — but it helps to know what is there.

Documentation, always available

Your environment, once connected

Choosing between environments

Every tool that reaches Dataverse takes an optional environment — a name such as test, a position such as 2, or the URL — and uses the primary without one. A name that matches nothing is refused with the list of environments that do exist, never matched to the nearest one.

Adding an environment needs a sign-in, so only login does it. The rest your assistant can do when you ask. Moving an environment to position 1 changes where every later write goes by default, so set_environment_index asks you first when it would change the primary, and remove_environment always asks.

The records themselves

Metadata says what the shape is; these say what is actually in it — which option-set value a row really carries, whether a lookup is populated, whether the row your code just wrote came out right. An agent that cannot look will guess instead.

Reading reads as you — the service principal or the signed-in user, never a portal user. None of the portal’s table permissions, column permissions or row filters apply, so this shows what is in the environment and not what a given portal user would be shown. A saved view’s FetchXML runs verbatim.

Getting a new project running

get_upgrade_plan is the one to call before changing a version string. It reads the version this project references, works out which releases lie between that and the target, and returns every breaking change in them with its migration instructions — alongside the files that name a version, including the server pin in .mcp.json that nothing else would catch. Bumping the packages without it produces build errors whose cause is several releases back.

The managed solution

The framework needs PowerPortalsProCore installed before a portal will run, and PowerPortalsProLocalization if you localize. These fetch a release and import it without the Power Platform CLI, so the environment they reach is the one this folder is bound to rather than whichever one pac happens to be pointed at.

An import is long-running, so install_solution starts it and hands back a job id; get_solution_import_status reports progress and the outcome, with Dataverse’s own message when it fails.

Note

publish_customizations is the same action as Publish all customizations in the maker portal, so it publishes everything pending in the environment, not only your own changes — on a shared development environment that can make a colleague’s half-finished work live. It is not needed after install_solution: importing a managed solution publishes what it brings.

Your own solution

Changes the server makes to an environment — a security role created, a privilege granted or taken back — go into a solution of your own, recorded for that environment in .ppp/mcp.json. create_solution sets up the publisher and the solution once, in the environment you build in; until one is recorded, the tools that customize Dataverse refuse, because a change made outside a solution is stranded in the Default solution where nothing can export it.

The binding also records whether the solution travels unmanaged (the default, for environments that are worked in too) or managed (for environments that only receive it). export_solution raises the version’s last part and saves the zip in the workspace, and push_solution exports it and imports it into another environment bound to the same folder — build in dev, push to test and production. A solution that depends on Power Portals Pro’s own fails to import until the target has that one too.

Portal roles

A portal role is half code and half data. The developer decides what the name “Dealer” grants; an administrator decides who holds it; nothing joins the two but the string. These maintain the Dataverse half from the same place the AddPortalRole call is written.

The string is the whole join. A typo between the registration and the record produces a role that is registered, assignable, and grants nothing — with no error anywhere. Letting the agent that just wrote the registration create the matching record closes that gap, and check_setup catches the mismatches already there: it compares every role name written in the project with the records, and reports any that are missing or inactive.

A Portal Role Configuration row decides which newly registered users are given a role: every one of them, or those matching FetchXML criteria. Criteria are checked against the rules the portal applies before the row is written, because the portal fails closed on bad criteria and a broken rule looks exactly like a rule that correctly matched nobody. Administrators edit the same rows on the portal's Role Configuration page.

Note

Deleting a role throws away every assignment of it and nothing remembers who held it. Deactivating grants nothing and mints no claim either, but keeps the assignments, so it is the reversible way to switch a role off. Both delete tools ask you to confirm first.

Licensed websites

A Power Portals Pro Website record is what licences a portal served from a given URL. These list, add and maintain them — typically once per environment, and again when a portal moves to its production host.

This is the first thing to check when every endpoint returns 402. Without a matching active record, each data endpoint on that host refuses the request. Deleting a record takes the licence with it from the next request onward, so prefer deactivating while you investigate; the delete asks you to confirm.

Azure deployment

Most of these tools only read. Those need nothing but Reader on the subscription, and they answer the questions that cost people days. check_deployment is the one to know: it compares what the project needs against what the App Service actually has, and names the failure — a required setting that only ever existed in your user secrets, the environment left on Development, WebSockets off on a Blazor portal, a Portal Website record that does not cover the deployed host, a portal that cannot reach Dataverse.

Seven tools change the App Service: create_app_service, configure_app_service, set_app_service_settings, deploy_to_app_service, restart_app_service, create_deployment_slot and swap_slots. Each of them asks you to confirm and tells you what it will cost before you answer — the real monthly figure for your region, read from Azure’s public price list, with a note that retail list ignores any Enterprise Agreement or reservation you hold. New resources default to the cheapest tier that can stay warm, for every environment including production, on the view that scaling up later is a minute and over-provisioning is a bill nobody revisits.

On a Standard or Premium plan, a portal can have deployment slots, each running against its own Dataverse environment — a uat slot against UAT, while production runs against production. create_deployment_slot creates one and links it to the environment, so when you are working in UAT and ask for a deploy, the build goes to the UAT slot rather than to production. The settings that name an environment are marked to stay with their slot, so swap_slots can promote the tested build to production and it starts against production’s data. A swap is refused when that would carry one environment into another.

Note

These do not use the Azure CLI, and never read or change its signed-in state. Run ppp-mcp azure login in the folder: like the Dataverse binding, the subscription belongs to that folder alone, so several projects can target different customers at once. az account show tells you nothing about what these tools will do.

Entra app registrations

The two registrations a portal needs, and the jobs that follow them. These are the steps that stall most setups: they are two separate registrations rather than one, the sign-in app needs a reply URL carrying a port the template randomised, and a client secret lapses a year later and stops the portal starting with an error that never mentions Entra.

The tools hold no rights of their own. Every call goes out on a delegated token for the account you signed in with, issued to a public client that holds no secret — so each operation succeeds exactly when you could have performed it by hand in the portal, and is refused with a 403 naming the directory role you would need when you could not. Sign in as an account holding Application Developer or above and these work; sign in as anyone else and they cannot do harm. No API permissions are requested and no admin consent is granted, because neither registration needs any.

Both halves, because neither works alone. An app registration gets a token; a Dataverse application user decides what that token may do. A registration without one authenticates perfectly and is then refused by every call, which reads as a broken connection rather than a missing object — and an application user holding no security role is created, visible, apparently fine, and permitted to do nothing. create_dataverse_application_user does the second half and assigns the role in the same step. This is the one place the server writes outside its own three tables, admitted because an application user is infrastructure rather than customer data; no tool creates, changes or deletes a business record.

They work on registrations that already exist. Each create_*_registration tool accepts a clientId, and with one it adopts that registration instead of creating one — adds a secret, merges in any new reply URLs, writes the credentials where you asked. The confirmation then says add a secret to the existing registration rather than create, so you are never approving one operation and getting another. The tools that change an existing registration, add_app_registration_redirect_uri and add_app_registration_secret, take a client id too, and prefer it: display names are not unique, and a tenant with three registrations called “Portal” is normal.

A created client secret is not returned by default. It goes straight from the Graph response into the project’s user secrets, an App Service application setting, or both, and the tool reports where it went and when it expires rather than what it was. When a secret has to reach somewhere the server cannot write — a colleague’s machine, a CI secret store, a vault in another subscription — ask for destination: "return" and the value is shown for you to copy. The confirmation says so before you agree, and the result carries the warning that earns it: the secret is then in the transcript, so treat it as exposed if that transcript is stored, and rotate it if in doubt. Secrets are minted for 24 months by default — the longest Entra’s portal offers — because the failure this framework actually suffers is a lapsed secret rather than a stolen one: the portal stops starting on a date nobody noted, with an error that never mentions Entra. That cap belongs to the portal’s UI, not the API, so expiryMonths accepts anything up to 120 (10 years) when you would rather rotate once every five years than risk a lapse. It is not the default, because a credential no administrator could have created by hand is a surprising thing to find — so the confirmation tells you how many years you asked for and that the portal could not produce it.

An app can sign in with a certificate instead of a secret. add_app_registration_certificate creates a certificate in Key Vault whose private key can never be exported, and adds its public half to the registration. Bind an environment to that app with ppp-mcp login --environment <url> --client-id <id> --certificate <url> and the vault signs each sign-in for whoever holds Key Vault Crypto User on it, so no secret sits on any machine. To rotate, run the same tool again; remove_app_registration_certificate retires the old certificate once nothing uses it.

Note

Two things are deliberately missing. Deleting a registration can take down sign-in for every application in a tenant and is recoverable only inside a 30-day window. Granting admin consent is the operation that turns one narrow delegated act into a standing tenant-wide permission. Neither is needed to get a portal running, so neither is offered. Creating the Dataverse application user for the new client id is a separate step too — the tool output gives you the command.

Removing the secret that expires

A client secret is the part of a deployed portal that stops working on a date nobody wrote down. These remove it, or move what is left somewhere it can be rotated without a redeploy.

Set D365__ManagedIdentityId to the identity’s client id — not the object or principal id the Azure portal shows first — and give that client id its own Dataverse application user, with create_dataverse_application_user like any other. The portal then connects with no secret at all.

The email sender

The portal sends confirmation and password-reset mail from an address that has to resolve to a queue or an enabled user in Dataverse. create_email_queue makes the queue, approves its mailbox and runs Test & Enable, reporting the result; check_email_sender explains why a sender is not delivering. A queue, rather than a person’s mailbox, is the right sender for a no-reply address.

Important

Two things it cannot do for you. The address needs a mailbox in Exchange Online — a shared mailbox, which needs no licence — and only an Office 365 Global Administrator or Exchange Administrator may approve it. When either is missing the tool says which, and running it again once it is fixed finishes the job. Until Test & Enable passes, Dataverse accepts the portal's mail and delivers nothing — indistinguishable from success from the portal's side, which is why the tool tests it rather than reporting a clean finish.

Working in other languages

The knowledge the server returns is English, and that is deliberate. Your assistant is the one reading it — you never see a tool response directly — so it translates for you: ask in your own language, and it answers in your own language. What it must not translate is the code. Component, parameter and column names are English in every project, whatever language the conversation is in, so keeping the reference English is what stops an assistant inventing a translated parameter name that does not exist.

Note

Questions that name something — “MainGrid Ansichten konfigurieren” — already work in any language, because component names are the same everywhere. The tools also tell your assistant to search in English, so questions that name nothing work too.

Troubleshooting