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:
- The API surface is too large to remember. Several hundred components across both stacks, each with dozens of parameters. An agent that half-remembers a parameter name writes something plausible that does not compile — or worse, compiles and does nothing.
- Your Dataverse is not in any document. Table and column logical names, relationship schema names, option-set values and — above all — saved view GUIDs exist only in your environment. Every code sample on this site writes a view id as an ellipsis, because there is nothing else it could write.
What it answers
- The framework — the full parameter and prop reference for every component on both stacks with types, defaults and permitted values; complete working samples taken verbatim from this site; the guides; the conventions from
AGENTS.md; and the Blazor-to-React parity matrix. It is version-matched to the packages you have installed. - Your environment, read-only — the tables and columns you actually have, which editor each column maps to, relationship names, option-set values, and your saved views with their real GUIDs.
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:
claude mcp add --scope user powerportalspro -- dnx PowerPortalsPro.Mcp --yes
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:
claude mcp add --scope project powerportalspro -- dnx PowerPortalsPro.Mcp --yes
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:
claude mcp add --scope project powerportalspro -- dnx PowerPortalsPro.Mcp@8.0.0 --yes
Or write the file yourself, at the root of your solution:
{
"servers": {
"powerportalspro": {
"type": "stdio",
"command": "dnx",
"args": [ "PowerPortalsPro.Mcp", "--yes" ]
}
}
}
Which file your editor reads
Visual Studio 2022 17.14+ and Visual Studio 2026 read
.mcp.jsonfrom the solution folder, and so does Claude Code. VS Code reads.vscode/mcp.jsonand Cursor reads.cursor/mcp.json— both use the sameserversblock, 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:
dotnet tool install --global PowerPortalsPro.Mcp
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 with
dnx, unpinned — the machine-wide entry above — needs nothing run.dnxresolves the latest version from your NuGet feeds each time your client starts the server, so a new release arrives the next time you restart the client. - Pinned in a project’s
.mcp.json. The templates writePowerPortalsPro.Mcp@<version>, matched to the framework packages that project uses. Change that version when you upgrade the packages, so the answers keep describing the framework the project actually runs.
Installed as a global tool (dotnet tool install --global PowerPortalsPro.Mcp, which puts ppp-mcp on your PATH):
dotnet tool update --global PowerPortalsPro.Mcp
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:
dnx PowerPortalsPro.Mcp login
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:
dnx PowerPortalsPro.Mcp login --environment https://contoso-dev.crm.dynamics.com --name dev
dnx PowerPortalsPro.Mcp login --environment https://contoso-test.crm.dynamics.com --name test
dnx PowerPortalsPro.Mcp login --environment https://contoso.crm.dynamics.com --name prod
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 binding —
.ppp/mcp.jsonin the folder. It lists the environments in order, with the name and account for each, holds no secrets, and is safe to commit: a teammate cloning the repository is pointed at the right environments without being told. - Your token — kept in your user profile and encrypted by the operating system keystore (DPAPI on Windows, Keychain on macOS, libsecret on Linux). It is never written into the repository. If no keystore is available the sign-in refuses rather than leaving a refresh token on disk in the clear.
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.
login— sign in and bind an environment to this folder, after any already bound. Options:--environment <url>,--name <name>,--make-primary,--pac-profile <name>,--username <upn>,--tenant <id>,--browser. Name an environment that is already bound —--environment test— to sign in to it again.status— which environments this folder uses and which is the primary, where each came from, which account is cached, and whether each connection works.logout— remove an environment from this folder:--environment <name>for one,--allfor every one. Add--forgetto remove the saved sign-in as well.environments— list the Power Platform CLI profiles available to mirror.azure—azure login,status,logoutandsubscriptions: binds this folder to an Azure subscription for the deployment tools.
Other ways to connect
The environment and credentials are resolved in this order, so the most explicit statement of intent wins:
- Environment variables —
PPP_D365_URL,PPP_D365_CLIENT_IDandPPP_D365_CLIENT_SECRETin the server'senvblock. A service principal, and the way to wire this up in CI or a shared container.PPP_D365_URLbecomes the primary environment, ahead of any the folder is bound to, and the service principal is used for the primary only. - The folder's sign-in — written by
login. The normal path. - The project's own settings — the same
D365:Url,D365:ClientIdandD365:ClientSecret(orD365:Secret) the portal itself reads from user secrets andappsettings. 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:
dnx PowerPortalsPro.Mcp environments
dnx PowerPortalsPro.Mcp login --pac-profile "Acme Dev"
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 selectis 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
find_parity Whether a component exists on the other stack
get_component_api Every parameter or prop, with types, defaults and permitted values
get_conventions The golden rules from AGENTS.md
get_doc One documentation section in full
get_example A complete working sample, verbatim
get_page_recipe Guide, both stacks' samples and API links for one topic
list_components Browse what the framework ships
search_docs Open questions across the guides, API reference and samples
whats_new Release notes
Your environment, once connected
describe_table Columns, and the editor each one maps to
list_choices Option-set values and labels
list_relationships Schema names for SubGrid and ManyToManyLookupEdit
list_tables Find a table's logical name
list_views Saved views with their real GUIDs
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.
dataverse_status Every environment this folder has, which is the primary, and whether each connects
remove_environment Stop this folder reaching an environment
link_environment_slot Tie an environment to an App Service deployment slot
rename_environment Call an environment what you call it: dev, test, prod
set_environment_index Reorder the environments; position 1 is the primary
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.
get_record One row by id, a column per line so a wide record stays readable
read_records A page of any table, or anything FetchXML can express
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
check_setup What this folder still needs before the portal will start
get_setup_plan The steps to a running portal, and what each one needs
get_setup_script A reviewable script that performs them, with your values filled in
get_upgrade_plan Moving to a newer release: what breaks, and which files name a version
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.
get_solution_import_status How an import started by install_solution is going
install_solution Download a managed solution and import it
list_solutions What is installed, and which releases are available
publish_customizations Make pending maker-portal changes live
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_customizationsis 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 afterinstall_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.
configure_solution Record an existing solution, or change how it exports
create_solution A publisher and an unmanaged solution, recorded for the environment
describe_solution The recorded solution, its version and what it holds
export_solution Save it under solutions/, managed or unmanaged as recorded
get_solution_export_status Finish an export that took longer than the tool waits
push_solution Export it and import it into another of the folder's environments
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.
create_portal_role A role for an AddPortalRole registration to bind to
create_portal_role_configuration Decide which new users are given a role
delete_portal_role Remove a role, and every assignment of it
delete_portal_role_configuration Stop giving a role to new users, leaving the role in place
list_portal_roles The roles, and which of them new users start with
update_portal_role Rename, describe, or activate/deactivate a role
update_portal_role_configuration Change which new users are given a role
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.
create_portal_website Licence a new URL
delete_portal_website Remove the record, and the licence with it
list_portal_websites The URLs licensed here, and whether each has a key
update_portal_website Correct a URL, set a new key, or deactivate one
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.
azure_status Which subscription this folder deploys to, and whether it connects
check_deployment What the project needs vs. what is actually deployed
configure_app_service Apply the recommended platform settings to an existing app
create_app_service Resource group, plan and web app, configured for your stack
create_deployment_slot A slot that runs against one of your environments, linked to it
deploy_to_app_service Build with dotnet publish and push the result
describe_app_service One app's platform configuration and setting names
enable_app_service_logging Send an app's logs to a workspace of its own, kept 60 days
get_deployment_plan The steps, and every plan tier with its current price
list_app_services The web apps in the subscription
list_deployment_slots The slots, the environment each runs against, and the links
restart_app_service Restart the web app, after confirming
set_app_service_settings Merge application settings
swap_slots Promote a slot's build, each slot keeping its own environment
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 loginin the folder: like the Dataverse binding, the subscription belongs to that folder alone, so several projects can target different customers at once.az account showtells 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.
add_app_registration_certificate Sign in as an app with a Key Vault certificate, no secret
add_app_registration_federated_credential Let a managed identity be an app's credential, no secret
add_app_registration_redirect_uri Add the production reply URL after a deployment
add_app_registration_secret Rotate a secret before it lapses
add_table_privileges_to_role Give your own role access to a table (never a managed one)
assign_application_user_column_profile Grant the secured contact columns a role cannot
assign_application_user_role Grant the application user a security role
create_dataverse_app_registration The backend identity the portal reads Dataverse as
create_dataverse_application_user The Dataverse half, with its security role
create_security_role An empty role of your own, for your tables' privileges
create_signin_app_registration The app behind "Sign in with Microsoft"
delete_security_role Delete a role of your own that nobody holds any more
describe_role_table_privileges What a role grants on one table, and at what depth
get_application_user Does Dataverse know this client id, and what may it do?
list_app_registrations Client ids, reply URLs and secret expiry dates
list_security_roles What roles the environment offers
remove_app_registration_certificate Retire an old certificate after a rotation
remove_application_user_column_profile Take back the secured contact columns
remove_application_user_role Take back a security role
remove_table_privileges_from_role Take a table's privileges back from a role, or lower them
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.
create_key_vault A vault the managed identity can read, for the secrets that remain
create_managed_identity A user-assigned identity, independent of any one app
enable_managed_identity Attach a managed identity to the App Service
list_user_secret_keys The keys in the project's user secrets — names, never values
push_secrets_to_key_vault Copy the user secrets you approve into the vault, and point the app at them
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.
check_email_sender Why mail from the sender address is not arriving, and what to fix
create_email_queue A queue for the portal's outbound mail, approved and tested
list_email_queues The queues in the environment, and the addresses they send from
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
- “No Dataverse environment is bound to this project folder” — run
loginin that folder. Check you are in the folder you think you are:statusprints the workspace root it resolved. - “The saved sign-in is no longer valid” — the refresh token expired or was revoked, or the tenant now requires a fresh interaction. Run
loginagain. - “The operating system keystore is not available” — common on a headless Linux box with no libsecret daemon. Use a service principal there instead, via the
PPP_D365_*environment variables. - Signed in, but the environment rejects the connection — the account authenticated but has no access to that environment. Check the URL, or sign in as a different user with
--username.
