PowerPortalsPro.Mcp 9.0.0

{
  "servers": {
    "PowerPortalsPro.Mcp": {
      "type": "stdio",
      "command": "dnx",
      "args": ["PowerPortalsPro.Mcp@9.0.0", "--yes"]
    }
  }
}
                    
This package contains an MCP Server. The server can be used in VS Code by copying the generated JSON to your VS Code workspace's .vscode/mcp.json settings file.
dotnet tool install --global PowerPortalsPro.Mcp --version 9.0.0
                    
This package contains a .NET tool you can call from the shell/command line.
dotnet new tool-manifest
                    
if you are setting up this repo
dotnet tool install --local PowerPortalsPro.Mcp --version 9.0.0
                    
This package contains a .NET tool you can call from the shell/command line.
#tool dotnet:?package=PowerPortalsPro.Mcp&version=9.0.0
                    
nuke :add-package PowerPortalsPro.Mcp --version 9.0.0
                    

Power Portals Pro — MCP Server

A Model Context Protocol server that gives AI coding agents accurate, first-hand knowledge of Power Portals Pro while you build a portal.

Without it, an agent writing Power Portals Pro code has to guess: parameter names it has never seen, and — worse — Dataverse view GUIDs, table and column logical names and option-set values that exist only in your environment and appear in no documentation anywhere. This server answers both.

What it knows

The framework, embedded in the package and version-matched to the release:

  • The full component API for both stacks — every parameter and prop with its type, default, permitted values and description.
  • Working, verbatim code samples, paired Blazor and React.
  • The conceptual guides from the documentation site.
  • The conventions from AGENTS.md, and the Blazor-to-React parity matrix.

Your environment, read live and read-only:

  • Tables, columns and the Power Portals Pro editor each column type maps to.
  • Saved views with their real GUIDs — the thing no document can tell an agent.
  • Relationship names for SubGrid and ManyToManyLookupEdit, and option-set values with labels.
  • Which portal roles exist, which of them new users start with, and which URLs are licensed.
  • The records themselves, in any table — read_records takes a table name or a FetchXML query (a saved view's FetchXML runs verbatim), and get_record fetches one row by id. Metadata says what the shape is; this says 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 is read-only and it reads as you — the service principal or the signed-in user, not a portal user. None of the portal's own table permissions, column permissions or row filters apply, so it shows what is in the environment and never what a given portal user would be shown. No tool creates, changes or deletes a business record.

The framework's own configuration

The exception to read-only is the framework's own configuration, and it is deliberately narrow: the tables below, and no others. Three are the framework's own; the rest are the plumbing a portal needs to run, and each is admitted for a stated reason rather than as a category. systemuser is only ever the application users that make an app registration usable — never a person's account — and queue/mailbox are only ever the sending address a portal's email goes out from.

Table Tools
Portal Role list_portal_roles, create_portal_role, update_portal_role, delete_portal_role
Portal Role Configuration create_portal_role_configuration, update_portal_role_configuration, delete_portal_role_configuration
Power Portals Pro Website list_portal_websites, create_portal_website, update_portal_website, delete_portal_website
System User (application users only) get_application_user, create_dataverse_application_user, assign_application_user_role, remove_application_user_role, assign_application_user_column_profile, remove_application_user_column_profile
Queue and Mailbox (the sending address only) create_email_queue
Security Role (privileges only, never the role itself) describe_role_table_privileges, add_table_privileges_to_role

Writes that never touch Dataverse at all — the App Service, Key Vault and managed identity in your Azure subscription, and the app registrations in your Entra tenant — are covered under deployment below. Every one of them asks before it acts.

Why roles. A portal role is half code and half data — the developer decides what the name "Dealer" grants, an administrator decides who holds it, and nothing joins the two but the string. A typo between them produces a role that is registered, assignable and grants nothing, with no error anywhere. Letting the agent that just wrote the AddPortalRole call maintain the matching record under the same name closes that gap, and check_setup catches the mismatches already there: it compares every role name written in the project — registrations, role checks and default roles — with the records, and reports any that are missing or inactive. FetchXML criteria is 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 maintain the same records on the portal's own Role Configuration page, which both templates include.

Why application users. An Entra 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, not a missing object — and an application user without a security role is created, visible, apparently fine, and permitted to do nothing. Both halves are infrastructure rather than customer data, the same category as importing the managed solution. Creating one needs System Administrator on the environment.

Why column security profiles. A security role can never grant a secured column — only a column security profile can, and System Administrator is the one role that gets past column security by carrying a profile implicitly. Power Pages secures two contact columns the framework writes as itself, adx_identity_logonenabled and adx_identity_passwordhash, so an application user moved onto any narrower role keeps signing people in and then fails at the first portal registration, with password sign-in unable to read stored hashes. The managed solution ships a Power Portals Pro Application User column security profile covering exactly those two; membership is not something a solution can carry, so assign_application_user_column_profile adds each application user to it — the app registration's, and the managed identity's a deployed app connects as.

Why security roles. The role that ships in the managed solution covers the framework's own tables and stops there, because no role authored here can know a consumer's tables — and under the application user needs access to them under both security models. Dataverse-native security is not an exemption: impersonation intersects privileges rather than delegating them, so the application user's role is a ceiling on what any portal user can do, however their own roles are configured. Granting that access by hand means finding one table in a grid of several hundred and clicking the correct arc of a circle eight times, which is done wrong quietly: a table missed surfaces much later as an empty grid or a save that fails with a privilege GUID in the message. This is the sharpest edge in the list — granting a privilege grants access to business records, even though no business record is touched — so it is also the most tightly bounded. Privileges are only ever added: a privilege already held at an equal or deeper scope is left exactly as it is, and nothing here revokes, creates or deletes a role. Consumers author their own role for their own tables and assign both: the shipped role can technically be edited, but it is our component and an upgrade can reapply it.

Why websites. Every data endpoint calls the licence check, which looks the request's host up in the website table. No matching active record means every endpoint returns 402 — a failure that names nothing an administrator can act on, and one that reliably strikes the first time a portal runs against a new environment or from a new URL. Licence keys can be written but are never read back: reads report only whether a key is set.

Deletes ask first. Where your client supports elicitation, the server asks you directly and the deletion proceeds only if you confirm — the agent cannot answer on your behalf. Where it does not, the tool refuses unless the caller passes the record's own name back in a confirm argument, which at least puts the name in the tool call you approve. Deactivating is offered as the reversible alternative everywhere it would do, and the tools say so.

Installing the managed solution

list_solutions shows what the environment has against what has been published, and says plainly whether PowerPortalsProCore is missing or behind. install_solution downloads a release and imports it — the latest by default, or any version you name — and get_solution_import_status follows it.

This exists because of the scope problem. pac auth selects one environment for the whole machine, so pac solution import can land in a different customer's org than the repository you are sitting in, and nothing in the command would tell you. This server's binding is per folder, so the environment follows the code. Before an import starts you are shown which environment is about to change and asked to confirm; where your client cannot prompt, the tool refuses until the environment URL is passed back as confirm.

Imports take minutes, so the tool starts one and returns a job id rather than blocking — a tool call that hangs for ten minutes is one your client gives up on while the import is still running. Unmanaged customizations are never overwritten: an import that genuinely conflicts fails and says so, rather than silently discarding someone's work in the maker portal.

The solutions are public, and can still be downloaded by hand from the releases page if you would rather import them yourself.

publish_customizations is the companion for the other half of that loop: when the portal is serving stale metadata after a change in the maker portal — a new column not appearing, a renamed label still showing the old text — this is the same action as the maker portal's "Publish all customizations". It publishes everything pending in the environment rather than only your own changes, which is worth knowing on a shared development environment. Importing a managed solution publishes what it brings, so it is not needed after install_solution.

Upgrading to a new release

get_upgrade_plan answers the question an upgrade starts with: what version is this project on, what would it be on, and what breaks in between. It reads the version the project references, resolves the newest published release, and returns the releases in between with every breaking change in them and its migration instructions — plus the files that name a version, which are never only the project files: the React client's package.json, and the server pin in .mcp.json that nothing else would catch.

That last one is why the tool exists in this shape. Upgrade the packages and leave .mcp.json pinned, and nothing fails — the server carries on answering for the version you left, so the first thing it tells you about the release you moved to is that it does not exist. The plan names the right form of the version in each place, too: a release is tagged with four parts and its packages are published with three, so the release version written into package.json fails at install on a version npm will not parse.

The tool writes nothing. install_solution upgrades the Dataverse side, the file edits are yours, and the plan is a sequence you can stop halfway through.

One limit worth knowing: the knowledge index ships inside this package, so a pinned server only knows about releases up to its own version. Asked to plan a move to something newer, it says so rather than reporting a clean upgrade — re-pin, restart the client, and ask again.

Install

There are two scopes, and both are worth having.

Once per machine, so it is available in any folder — including an empty one you are about to scaffold a project into, which is exactly where it can tell you which template to use and what the options mean:

claude mcp add --scope user powerportalspro -- dnx PowerPortalsPro.Mcp --yes

Visual Studio reads a user-level .mcp.json from your profile folder and Cursor uses ~/.cursor/mcp.json; both take the same servers block shown below.

Per project. Projects created by the dotnet new powerportalspro templates are wired up already, with the version pinned to the packages that project uses. To add it to an existing project, run this in the project folder:

claude mcp add --scope project powerportalspro -- dnx PowerPortalsPro.Mcp --yes

That writes the .mcp.json below for you. Note what it does not do: it leaves the server unpinned, so the folder gets the newest published server rather than the one matching the PowerPortalsPro.* packages the 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 as .mcp.json:

{
  "servers": {
    "powerportalspro": {
      "type": "stdio",
      "command": "dnx",
      "args": ["PowerPortalsPro.Mcp", "--yes"]
    }
  }
}

Visual Studio 2022 17.14+ and Visual Studio 2026 read .mcp.json from the solution directory. For Claude Code the same file works with the servers listed under mcpServers instead. For VS Code the file is .vscode/mcp.json, and for Cursor it is .cursor/mcp.json; both use the servers shape shown above.

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.

Both entries use the same server name, so a client that understands both scopes uses the project's: version-matched answers inside a project, latest ones everywhere else.

Updating

How you update depends on how you installed it.

Installed with dnx, unpinned — the machine-wide entry above — needs nothing run. dnx resolves 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 write PowerPortalsPro.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

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 work with no configuration at all. The metadata tools need a connection, and that connection belongs to the folder, not to the machine. Run this once in a project:

dnx PowerPortalsPro.Mcp login

It signs you in, writes .ppp/mcp.json in the folder, and stores the token encrypted in your user profile. Other projects are unaffected — a consultant can have agents working against three customers at the same time, each in its own folder, each signed in to its own environment.

dnx PowerPortalsPro.Mcp login --environment https://acme.crm.dynamics.com
dnx PowerPortalsPro.Mcp login --pac-profile "Acme Dev"   # take the environment from a pac profile
dnx PowerPortalsPro.Mcp login --browser                  # browser instead of a device code
dnx PowerPortalsPro.Mcp status                           # what is this folder bound to?
dnx PowerPortalsPro.Mcp environments                     # pac profiles available to mirror
dnx PowerPortalsPro.Mcp logout [--forget]

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

What goes where

Where Committable
Which environment this folder uses .ppp/mcp.json in the repository Yes — it holds no secrets, and it points a teammate at the right environment
Your token Encrypted in your user profile — DPAPI on Windows, Keychain on macOS, libsecret on Linux Never written to the repository

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 is always a separate sign-in.

If your machine has no usable keystore, the sign-in refuses rather than writing a refresh token to disk in the clear; use a client secret there instead.

Other ways to connect

Resolved in this order, so the most explicit statement of intent wins:

  1. PPP_D365_URL / PPP_D365_CLIENT_ID / PPP_D365_CLIENT_SECRET in the server's env block — a service principal, for CI or a shared container.
  2. The folder's sign-in from ppp-mcp login.
  3. The project's own settings — the same D365:Url, D365:ClientId and D365:ClientSecret (or D365:Secret) the portal reads from user secrets and appsettings. A real Power Portals Pro project therefore often needs no sign-in at all.

About the Power Platform CLI

pac has no command that emits 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 account from a profile you already have and sign in as that account — usually without prompting, since you already have a session.

We never run pac auth select. That switches a single machine-wide active profile, which would move every other project at once; it is the behaviour this design exists to avoid.

Deploying to Azure

The server can also stand up and deploy the App Service a portal runs on. Bind the folder to a subscription once:

dnx PowerPortalsPro.Mcp azure login

It does not use the Azure CLI, and never reads or changes its signed-in state. az login sets one active subscription for every process on the machine — the same machine-wide behaviour as pac auth select, and the same reason this design avoids it. The subscription binds to the folder, so several projects can target different customers at once.

azure_status, list_app_services, describe_app_service What is there. Reader is enough.
get_deployment_plan The steps, and every plan tier with its current price from Azure's public price list. Needs no sign-in.
check_deployment What the project needs vs. what is actually deployed.
create_app_service, configure_app_service, set_app_service_settings, deploy_to_app_service The changes. Each one confirms first.
create_managed_identity, enable_managed_identity, create_key_vault An identity for the app, and a vault its secrets live in rather than its configuration.

check_deployment is the one to know. Deployments of this framework fail in a handful of ways and every one of them is invisible where it goes wrong: a configuration key that only ever existed in your user secrets, so the site never starts; ASPNETCORE_ENVIRONMENT left on Development, so a React portal's deep links redirect to localhost; WebSockets off, so a Blazor circuit silently degrades to long polling; a Portal Website record naming a different host, so every data endpoint returns 402 while the pages render perfectly. All of those are a comparison between two things the server can already see, and it needs nothing but Reader — so it is safe to run against production, which is where it is needed.

Every tool that changes anything asks you first, and tells you the cost. The confirmation carries the real monthly figure for your region, read live from Azure's retail price list, and says plainly that retail list ignores any Enterprise Agreement, reservation or dev/test rate you hold. Where your client supports elicitation the server asks you directly and the agent cannot answer on your behalf; where it does not, the tool refuses unless the caller echoes the resource name back in a confirm argument.

New resources default to B1 — for every environment, production included. It is the cheapest tier that can stay warm, and scaling up is one call with no redeploy; provisioning a premium plan because something is "production" starts a bill nobody revisits. Application setting values are never read back, with a short allowlist of documented non-secrets such as ASPNETCORE_ENVIRONMENT and D365__Url — a portal's settings hold the Dataverse client secret and every sign-in provider's secret, and those must not reach a transcript.

dnx PowerPortalsPro.Mcp azure login --subscription "Contoso Production"
dnx PowerPortalsPro.Mcp azure login --tenant <id>   # when you are a guest in the customer's tenant
dnx PowerPortalsPro.Mcp azure status
dnx PowerPortalsPro.Mcp azure subscriptions
dnx PowerPortalsPro.Mcp azure logout [--forget]

Secrets in Key Vault, not in configuration

Storing a client secret as a literal application setting works, and every deployment starts there. What it costs is that the secret is then readable by anyone with Contributor on the app, shows up in an export of the configuration, and is one copy-paste from a chat window.

create_managed_identity makes a user-assigned identity that holds no credential; enable_managed_identity attaches it to the app; create_key_vault creates a vault and grants that identity read access, and the signed-in account write access. After that the secret tools take destination: "key-vault" — a freshly minted client secret goes from Graph straight into the vault, and the app setting becomes a @Microsoft.KeyVault(...) reference that App Service resolves at startup. The value never becomes a literal setting.

Set D365:ManagedIdentityId to that identity's client id and the Dataverse connection stops using a client secret at all — there is then nothing left to expire. The identity needs a Dataverse application user, created from the same client id, which create_dataverse_application_user does.

Two things worth knowing. Owner on the subscription does not grant access to secret values — Key Vault separates its control plane from its data plane deliberately, which is why the roles are assigned explicitly even to you. And a vault name is reserved for 90 days after deletion: soft delete cannot be turned off, so recreating a vault under the same name inside that window needs an explicit purge. It is the only resource these tools create that is not cheaply undoable.

User-assigned, not system-assigned. A system-assigned identity is a property of the app: it is created and destroyed with it, and a deployment slot gets a different one. Either event mints a new identity and orphans everything built on the old one — a Dataverse application user most of all, which then matches no token while looking perfectly healthy. A user-assigned identity is a resource of its own and survives the app being torn down and rebuilt, which for a portal that gets rebuilt from a template more than once is the difference between setting this up once and setting it up every time. enable_managed_identity will turn on a system-assigned identity if you omit the identity id, and says what you are giving up when it does.

The portal's email sender

D365:EmailSenderEmailAddress is a required setting, and the framework resolves it to a Dataverse user, then a queue, then a team. Use a queue: it is the only option needing no work in Exchange, because the mailbox Dataverse creates for it points at the organisation's existing Exchange Online profile — nothing to provision, no licence for the address. Pointing it at a real person instead puts their name on every automated message, sends replies to their inbox, and breaks the portal when they leave.

create_email_queue creates the queue and approves the address — both halves of it, since the queue's own approval and the mailbox's isemailaddressapprovedbyo365admin are separate flags on separate records and a queue is created pending on both. Test & Enable Mailbox in the Power Platform admin centre remains a single manual click.

That click matters more than it looks: until it has run, Dataverse accepts the email, records it and never delivers it — which from the portal's side is indistinguishable from a successful send. Nothing throws and nothing is logged. If registration mail is not arriving, check the mailbox's outgoing status first; Not Run means this step was missed.

Entra app registrations

The two registrations a portal needs, and the jobs that follow them:

list_app_registrations             Client ids, reply URLs and secret expiry dates
create_dataverse_app_registration  The backend identity the portal reads Dataverse as
create_signin_app_registration     The app behind "Sign in with Microsoft"
add_app_registration_redirect_uri  Add the production reply URL after a deployment
add_app_registration_secret        Rotate a secret before it lapses

They work on registrations that already exist. Pass clientId to either create_* tool and it adopts that registration instead of creating one — adds a secret, merges in any new reply URLs, writes the credentials where you asked — which is the common case in a tenant that has been running for a while. The confirmation says "add a secret to the EXISTING registration" rather than "create", so you are never approving one operation and getting another. The maintenance tools take clientId too, and prefer it: display names are not unique, and a tenant with three registrations called "Portal" is normal.

These 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 they 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.

A created client secret is not returned by default. It goes from the Graph response straight 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.

Sometimes a secret has to reach somewhere this server cannot write — a colleague's machine, a CI secret store, a Key Vault in another subscription. Pass destination: "return" and the value is shown so you can copy it. The confirmation says so before you agree, and the result carries the warning it deserves: the secret is then in the transcript, so treat it as exposed if that transcript is stored, and rotate it with add_app_registration_secret if in doubt. Forbidding this outright would only be routed around by copying the value out of the Entra portal instead.

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 24 months is the portal's UI cap, not the API's. Graph accepts any expiry, so expiryMonths takes anything up to 120 (10 years) if 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 — and going past 24 is exactly the argument most likely to have been mistyped, so the confirmation says how many years you have asked for and that the portal could not produce it.

If the tenant has an application management policy capping credential lifetimes or forbidding password credentials, Graph refuses with a plain 400 whose message names neither the restriction nor the limit. The tools recognise that shape and say which knob to turn instead of reporting a malformed request.

Two things are deliberately absent. 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 a new client id is a separate tool rather than a separate chore: create_dataverse_application_user makes the systemuser row and assigns a security role in one confirmed step, and list_security_roles shows what the environment offers. Which role depends on whether you would mind the portal's credential reaching data the portal never shows. System Administrator is fine on a demo or a sandbox and a poor idea anywhere else; the alternative is two additive roles — Power Portals Pro Application User from the managed solution, which covers the framework's own needs, plus a custom role of your own for your tables, filled in with add_table_privileges_to_role. Impersonation intersects privileges rather than delegating them, so the application user's role is a ceiling on what any portal visitor can do.

Licensing

The server itself is not licence-gated — it is documentation and metadata, and the documentation half is the same content published at demo.powerportalspro.com. The framework's own website-licence check is unaffected and still applies to a running portal.

Use of the Power Portals Pro packages is governed by the EULA included in this package.

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

This package has no dependencies.

Version Downloads Last Updated
9.0.0 65 9/18/2026
8.1.0 70 9/15/2026
8.0.0 66 9/14/2026