@magic-alm/powerplatform
v0.5.0
Published
Microsoft Power Platform adapters for Magic ALM Matrix — Dataverse, PAC CLI, Power Automate, Power Apps, Power Pages, and Power Platform Admin API.
Readme
@magic-alm/powerplatform
Microsoft Power Platform adapters and a provider-neutral composite client for Magic ALM Matrix.
The package includes concrete Dataverse Web API and PAC CLI adapters, a Python SDK process bridge, and compatibility facades for existing consumers. New consumers should use the composite client as their stable boundary.
Power Platform projects commonly use Azure resources. Those shared Azure concerns stay in @magic-alm/azure; this package will only grow Power Platform-native adapters for Dataverse, PAC CLI, Power Automate, Power Apps, Power Pages, and Power Platform Admin API.
Model-Selected Task Providers
PowerPlatformScaffold has an opt-in task execution path separate from its legacy
preference/fallback router. An orchestration host registers immutable,
pre-authorized PowerPlatformExecutionTask values (taskId,
requestFingerprint, operation name/version, parameters, allowedProviderIds)
and one or more TaskExecutionProviders. MCP is the transport between hosts;
mcp is not accepted as a provider id.
Each provider implements describeCapabilities(), evaluateTask(task) and
execute(task). Capability descriptions carry exact operation versions,
public-safe constraints, explicit configuration, and available, unavailable
or unknown availability. They report host-supplied state; discovery never
probes an environment or initiates authentication.
resolveEligibility(taskId) is read-only disclosure. The version-1 result is
either status: "resolved" with the operation, the task's fingerprint, eligible
provider capabilities and safe exclusion codes, or status: "denied" for a task
the host did not register — a closed answer that enumerates nothing.
executeTask(taskId, providerId, requestFingerprint) rechecks the same
eligibility and dispatches exactly the selected provider; it never substitutes,
retries, or calls forceProvider.
The scaffold does not provide durable replay protection, approval issuance, or dependency scheduling; the orchestration host owns those. Existing non-task scaffold routing is unchanged and is not the model-selection enforcement path.
Sealed task envelope
A host that runs for an orchestrator is started with a sealed task envelope
rather than a bare task list. SealedTaskEnvelope.ts owns the schema
(contractVersion: 1) and the verifier; the orchestrator owns the signing key
and signs the envelope when it authorizes the run. The host is given only the
verification key, so a task file that was edited, replayed against another
environment, or produced without the signing key is refused before any task is
registered.
The Ed25519 signature covers the canonical JSON of issuer, issuedAt, the
binding (environmentUrl, solutionUniqueName) and every task. It does not
name a selected provider: the model chooses from the eligible subset and the
orchestrator locks that choice at first dispatch.
{
"contractVersion": 1,
"issuer": "magic-alm-autonomy",
"issuedAt": "2026-10-02T17:00:00Z",
"binding": { "environmentUrl": "https://org.crm9.dynamics.com", "solutionUniqueName": "CitFeedback" },
"tasks": [
{
"taskId": "identity-check",
"requestFingerprint": "<64-character SHA-256 fingerprint>",
"operation": { "name": "security.whoAmI", "version": "1" },
"parameters": {},
"allowedProviderIds": ["dataverse-webapi"]
}
],
"signature": { "algorithm": "ed25519", "keyId": "maa-2026-10", "value": "<base64>" }
}Ed25519SealedTaskEnvelopeVerifier returns verified with the parsed envelope, or
rejected with one of MALFORMED_ENVELOPE, UNSUPPORTED_CONTRACT_VERSION,
UNSUPPORTED_ALGORITHM, UNKNOWN_KEY, SIGNATURE_INVALID, BINDING_MISMATCH.
signSealedTaskEnvelope exists for tests and tooling; production signing is the
orchestrator's.
mam-powerplatform-mcp
The package exposes the mam-powerplatform-mcp stdio executable for an
orchestrator to start per run. It speaks the MCP stdio transport (one JSON-RPC
message per line, which is what VS Code Copilot and the reference SDKs send) and
also the Content-Length-framed form @magic-alm/core's StdioMcpAdapter uses;
the framing is detected from the client's first bytes and replies match it. It
reads:
POWERPLATFORM_ENVIRONMENT_URL: Dataverse target URL; must equal the envelope binding.POWERPLATFORM_ACCESS_TOKEN: caller-acquired bearer token; never read from or written to the envelope.POWERPLATFORM_SEALED_TASKS_FILE: path to the signed envelope.POWERPLATFORM_ENVELOPE_KEY_IDandPOWERPLATFORM_ENVELOPE_PUBLIC_KEY: the verification key (PEM;\nescapes accepted).POWERPLATFORM_SOLUTION_UNIQUE_NAME(optional): when set, the envelope's solution binding must equal it.
Startup verifies the envelope and registers its tasks. The host makes no
Dataverse call of its own: the first request the environment sees is the first
executeTask the orchestrator dispatches. A rejected envelope exits 1 with
POWER_PLATFORM_ENVELOPE_REJECTED_<reason> on stderr before the server starts —
the verifier's reasons plus UNREADABLE and FILE_TOO_LARGE for the file
itself — while a missing variable exits with POWER_PLATFORM_HOST_MISSING_<name>,
so a runner can tell the two apart by prefix.
createPowerPlatformMcpHost returns { server, tools, registration }: the stdio
server, the same PowerPlatformMcpTools surface it serves (for in-process
callers and tests), and the verified registration summary.
mam-powerplatform-seal is the operator counterpart: keygen writes an
Ed25519 pair, sign seals a tasks file into an envelope (deriving each task's
fingerprint from the issuer, the binding and the task), and verify checks one
against a public key. docs/powerplatform-copilot-canary.md walks the
read-only canary with VS Code Copilot as the MCP client.
The environment URL must be HTTPS: the host sends the access token as a
bearer header on every call. A loopback address (127.0.0.1, localhost,
::1) is the one exception, for local mocks; a plaintext remote target exits
POWER_PLATFORM_INSECURE_ENVIRONMENT_URL before the envelope is read.
Known limit of contract v1: no replay protection. issuedAt is signed and
format-checked but not checked for age or prior use, so a verified envelope can
be presented to the same environment again. That is acceptable while the only
registered provider is read-only, and must be closed — expiry plus single-use
registration on the orchestrator side — before a mutating provider is added.
The host serves three scaffold tools beside the package's adapter tools:
powerplatform.scaffold.describeRegistration (bindings, task ids, provider ids —
for an orchestrator preflight), powerplatform.scaffold.resolveEligibility, and
powerplatform.scaffold.executeTask. None is a model-facing discovery tool; the
orchestrator converts validated eligibility into the choices it discloses.
The one registered provider is read-only. DataverseWebApiTaskProvider supports
security.whoAmI@1, datastore.query@1 and metadata.describeEntity@1. The
query is bounded by the policy sealed inside the task (allowed containers and
fields, maxItems ≤ 100); the metadata description is bounded the same way
(allowed entity logical names and EntityDefinitions properties, at most 32),
issues only EntityDefinitions(LogicalName='…')?$select=… — the one request
shape the metadata endpoint accepts — and returns only the selected properties.
Anything outside either policy is rejected before HTTP. Mutating operations are
absent by design; adding one is a contract change. Build the package before starting the
executable; environment confirmation and token acquisition remain the
operator's.
Shared PAC CLI Runner
CLI-backed adapters should inject a command runner rather than shelling out directly:
import { runPacJson, type CommandRunner } from "@magic-alm/powerplatform";
const runner: CommandRunner = {
run(command, args) {
return Promise.resolve({ exitCode: 0, stdout: "{}", stderr: "" });
},
};
const result = await runPacJson({
runner,
authProfile: "mam-dev",
environmentUrl: "https://your-env.crm.dynamics.com",
}, ["solution", "list", "--json"]);When authProfile is set, the runner first executes pac auth select --name <profile> and then executes the requested PAC command. PAC does not support a global --auth <profile> flag on solution or pipeline commands. Use authProfileIndex when the host wants to select by PAC profile index instead of name.
runPacJson is for PAC commands that are expected to emit structured JSON. Empty stdout and non-JSON stdout are treated as adapter failures. Use runPac for PAC commands whose only reliable success signal is exit code 0.
REST-driven adapters accept narrow client-like dependencies through constructor options. This keeps SDK selection and authentication in the host application while concrete Power Platform mappings are added incrementally.
Built-In REST Client
Host applications can keep providing their own PowerPlatformClient, or use the built-in configurable REST client when a simple HTTP transport is enough:
import { DataverseDataStore, PowerPlatformRestClient } from "@magic-alm/powerplatform";
const client = new PowerPlatformRestClient({
baseUrl: process.env.POWERPLATFORM_ENVIRONMENT_URL!,
tokenProvider: async () => process.env.POWERPLATFORM_ACCESS_TOKEN,
routes: {
"dataverse.datastore.read": (input) => ({
method: "GET",
path: `/api/data/v9.2/${(input as { table: string; id: string }).table}(${(input as { id: string }).id})`,
}),
},
});
const store = new DataverseDataStore({ client });The built-in client maps adapter operation names to caller-supplied routes. It injects bearer tokens, serializes JSON request bodies for body-capable methods, parses JSON responses, and raises typed errors for missing routes or non-2xx responses. It does not hard-code tenant IDs, environment URLs, credentials, or a full Power Platform endpoint catalog.
Use a host-provided PowerPlatformClient when the application already has SDK clients, retry policy, tracing, or endpoint discovery. Use PowerPlatformRestClient for lightweight integrations, tests, and automation where route mapping in configuration is preferable.
PowerPlatformRestClient uses global fetch by default. In Node runtimes without global fetch, pass the fetch option explicitly.
Composite Power Platform Client
PowerPlatformCompositeClient is the provider-neutral boundary for new
consumers. It owns the operation contract and can route the same logical call
to PAC CLI, Dataverse Web API, or a Python SDK bridge. Successful calls return
normalized results; callers do not receive PAC CommandResult objects, raw
OData envelopes, or Python process responses.
import {
NodeCommandRunner,
PowerPlatformCompositeClient,
resolvePowerPlatformPythonSdkBridgePath,
} from "@magic-alm/powerplatform";
const client = new PowerPlatformCompositeClient({
pacCli: {
runner: new NodeCommandRunner(),
authProfile: "mam-dev",
environmentUrl: process.env.POWERPLATFORM_ENVIRONMENT_URL,
},
webApi: {
baseUrl: process.env.POWERPLATFORM_ENVIRONMENT_URL!,
tokenProvider: async () => process.env.POWERPLATFORM_ACCESS_TOKEN,
},
pythonSdk: {
scriptPath: resolvePowerPlatformPythonSdkBridgePath(),
operations: ["datastore.query", "solution.export"],
env: { MAM_POWERPLATFORM_PYTHON_ADAPTER: "my_mam_powerplatform_adapter:handle" },
},
});
const records = await client.execute("datastore.query", {
table: "accounts",
query: "name eq 'Contoso'",
});
const solution = await client.execute("solution.export", {
name: "core",
managed: true,
});
// solution.contentBase64 is stable regardless of which provider produced it.| Provider-facing payload | Composite result |
| --- | --- |
| OData { value, "@odata.nextLink" } | { items, continuationToken, hasMore } |
| ExportSolutionFile, exportedSolutionFile, or customizationFile | { solutionName, managed, contentBase64 } |
| PAC-generated solution ZIP | { solutionName, managed, contentBase64 } |
| Python { ok: true, data } response | The canonical operation result represented by data |
The default order is Dataverse Web API then Python SDK for datastore, metadata,
dataflow, and flow operations. Solution export and import prefer PAC CLI before
Dataverse Web API and Python SDK; solution check prefers PAC CLI before Python.
Unsupported providers and providers proven unavailable before execution can be
skipped. Retryable runtime failures only fall back for read-only operations. A
mutating operation is never replayed after a provider reports that a side effect
may have started, and a PAC validation failure remains terminal.
Web API transport failures are classified at the provider boundary: a network
error that proves the request never reached Dataverse (DNS, TCP connect, or TLS
setup failure) is treated as provider-unavailable and may fall back even for a
mutation, while any other transport failure during a mutation is ambiguous —
it is reported with retryable: false and a possibly-started side effect, and
is never replayed on another provider. An HTTP response of any status proves
the request reached Dataverse and keeps its existing error semantics. Existing typed
adapters can also use { client } with this composite because their namespaced
operation names are accepted as compatibility aliases.
Solution option translation follows Microsoft's
PAC solution command reference.
Import activation and publication remain distinct: publishWorkflows or
activatePlugins selects PAC --activate-plugins, while publishChanges
selects PAC --publish-changes; the Dataverse Web API equivalent imports first
and invokes PublishAllXml only after a successful import.
If a configured provider cannot honor a requested option, it is skipped before
execution instead of silently dropping that option.
The package ships a reference bridge at
python/powerplatform_sdk_bridge.py. It loads a host-owned Python module from
MAM_POWERPLATFORM_PYTHON_ADAPTER; that module owns the Microsoft Python SDK,
authentication, retries, and the handle(operation, input) implementation.
Use resolvePowerPlatformPythonSdkBridgePath() so deployments do not hard-code
the bridge's location inside the installed package.
NotImplementedError is translated to an unsupported-operation response. A
host exception may also expose code, retryable, and side_effect_started
attributes so the composite can make a safe fallback decision without leaking
the Python exception shape to its caller.
The bridge is a pluggable protocol boundary, not a bundled Dataverse SDK. The
host is responsible for supplying the adapter module on PYTHONPATH (or an
importable location), the Microsoft SDK it wraps, credentials and token
acquisition, retry policy, and a supported Python runtime (3.9 or later). A
minimal reference adapter used by the package tests is checked in at
src/__tests__/fixtures/python/sample_powerplatform_adapter.py.
When pythonPath is not configured, the driver resolves the executable per
platform: python on Windows and python3 elsewhere (see
resolveDefaultPythonExecutable). Production deployments — especially Windows
hosts, where python3 is typically absent and the resolved python may be a
Store alias — should pin an explicit pythonPath. If the executable cannot be
started, the driver fails with a provider-unavailable error naming the
executable and the pythonPath remedy, and no side effect is reported.
Dataverse Dataflows
DataverseDataflows targets the Microsoft-documented Dataverse Web API entity set
/api/data/v9.2/msdyn_dataflows. The entity reference
documents POST, GET, PATCH, and DELETE, the msdyn_dataflowid primary key, and the entity properties. The adapter defaults to v9.2, supports a configurable apiVersion, and does not infer alternate entity sets.
import { DataverseDataflows } from "@magic-alm/powerplatform";
const dataflows = new DataverseDataflows({
baseUrl: process.env.POWERPLATFORM_ENVIRONMENT_URL!,
tokenProvider: async () => process.env.POWERPLATFORM_ACCESS_TOKEN,
});
const created = await dataflows.create({
msdyn_name: "Orders",
msdyn_description: "Daily order import",
});
const firstPage = await dataflows.list({
filter: "statecode eq 0",
select: ["msdyn_dataflowid", "msdyn_name", "createdon"],
top: 50,
orderBy: "modifiedon desc",
});
const nextPage = firstPage.continuationToken
? await dataflows.list({ continuationToken: firstPage.continuationToken })
: undefined;
await dataflows.update(created.msdyn_dataflowid!, {
msdyn_description: "Updated daily order import",
});Static token, async tokenProvider, injected fetch, and { client: PowerPlatformClient } modes follow the same constructor conventions as the other Dataverse adapters. In Web API mode, continuation tokens are the opaque @odata.nextLink value returned by Dataverse; replaying one resolves authentication again, does not rebuild the original query, and requires the configured Dataverse origin and dataflow collection before attaching authorization. An injected PowerPlatformClient owns transport and authentication, and must return the normalized { items, continuationToken, hasMore } list shape rather than raw Dataverse JSON.
Create and update inputs type the documented non-read-only dataflow fields used by the adapter: msdyn_name, msdyn_description, msdyn_destinationadls, msdyn_emailsettings, msdyn_gatewayobjectid, msdyn_internalversion, msdyn_mashupdocument, msdyn_mashupsettings, msdyn_originaldataflowid, msdyn_refreshhistory, and msdyn_refreshsettings. Dataverse can still enforce environment-specific required fields, managed properties, feature availability, and licensing. Callers need the corresponding Dataverse CREATE, READ, WRITE, or DELETE access rights for each operation.
publish() and refresh() throw DataverseDataflowUnsupportedCapabilityError. The entity reference documents no dataflow publish or refresh action. Microsoft exposes a separate Power Query Dataflows connector RefreshDataflow action and a separate Power BI REST refresh endpoint; neither is treated as a Dataverse msdyn_dataflows operation. Add lifecycle support only through a separately authenticated, first-party contract with its own types and integration tests.
Module Formats
@magic-alm/powerplatform supports both CommonJS and ESM package consumers:
const { PowerPlatformRestClient } = require("@magic-alm/powerplatform");import { PowerPlatformRestClient } from "@magic-alm/powerplatform";The package keeps CommonJS compatibility at dist/index.js and exposes an ESM wrapper at dist/index.mjs through the package import export condition.
PAC CLI Auth Profiles
PAC-backed adapters receive an injected CommandRunner plus optional authProfile and environmentUrl values:
import { PacCliIac, PowerPlatformCI, type CommandRunner } from "@magic-alm/powerplatform";
const runner: CommandRunner = {
run(command, args, options) {
// Delegate to the host process runner, test double, or automation sandbox.
return Promise.resolve({ exitCode: 0, stdout: "{}", stderr: "" });
},
};
const pac = {
runner,
authProfile: "mam-dev",
environmentUrl: process.env.POWERPLATFORM_ENVIRONMENT_URL,
};
const iac = new PacCliIac(pac);
const ci = new PowerPlatformCI(pac);Create the matching PAC profile outside the adapter package, for example with pac auth create --name mam-dev .... The package does not read credentials directly and does not create PAC profiles for callers.
If you already have Azure CLI signed in, you can get a Dataverse access token for the current environment without using PAC export commands:
az account get-access-token --resource "https://gov.crm9.dynamics.com" --query accessToken -o tsvUse the Dataverse resource that matches your cloud and environment. For example, https://crm.dynamics.com is used for commercial tenants, while https://gov.crm9.dynamics.com matches the GCC/Gov Dataverse endpoints used in this repo.
PowerPlatformCI intentionally does not fabricate a provider-neutral CI/CD adapter over PAC pipeline commands. Current PAC pipeline support exposes deployment/list operations, but not the run status, artifacts, approvals, rollback, and run-handle semantics required by @magic-alm/core/cicd. Until that mapping is designed against a real environment, PowerPlatformCI methods throw UnsupportedOperationError instead of emitting invalid pac pipeline run/status/artifacts/approvals/rollback/show commands.
Composition Boundary
Do not duplicate shared Azure infrastructure adapters here:
- Entra ID identity: use
@magic-alm/azureEntraIdIdentity. - Key Vault references: use
@magic-alm/azureKeyVaultSecrets. - Application Insights reporting: use
@magic-alm/azureAppInsightsObservabilityandAppInsightsTelemetry.
The composite client is the recommended boundary for new consumers. Remaining package-local facades and provider-specific surfaces can continue to grow behind the same operation contract.
Adapter Capability Boundaries
This package exports concrete adapters only where the class owns provider-specific behavior. Classes that simply forward operation names to an injected PowerPlatformClient are documented as client facades until they map Dataverse Web API, Admin API, Power Automate, Power Apps, Power Pages, or PAC semantics directly.
The same classification is exported as powerPlatformAdapterCapabilities so consumers and tests can detect unsupported or facade-only surfaces.
| Surface | Current status | Boundary |
| --- | --- | --- |
| PowerPlatformCompositeClient | Real adapter | Provider-neutral operation boundary across PAC CLI, Dataverse Web API, and Python SDK bridge. It owns routing, safe fallback, compatibility aliases, and normalized successful results. |
| PowerPlatformRestClient | Real adapter | Configurable HTTP transport that maps operation names to caller-supplied routes, handles bearer auth, parses JSON, and raises typed route/HTTP errors. It is not a full Dataverse endpoint catalog. |
| PacCliIac | Real adapter | PAC-backed IaC surface for validated solution operations, auth selection, environment targeting, and mapped PAC failures. Additional PAC behavior must be validated before it is exposed. |
| PowerPlatformMcpTools | Real adapter | MCP tool surface built on package routing and safety helpers. Build Power Platform MCP adapters on top of @magic-alm/core/mcp, and only use context-mcp-gateway if those adapters need HTTP/SSE exposure. |
| DataverseDataStore | Real adapter | Maps the core datastore contract to Dataverse Web API CRUD, OData filters, ETags, PATCH, pagination through @odata.nextLink, and $batch. The injected client constructor option remains available as a compatibility facade for hosts that already own routing. |
| DataverseDataflows | Real adapter | Maps typed CRUD and OData list pagination to the documented Dataverse msdyn_dataflows entity set. Publish and refresh throw a typed unsupported-capability error because no first-party Dataverse lifecycle action is documented. Static tokens, async token providers, and injected clients are supported. |
| DataverseSecrets | Real adapter | Maps the core secrets contract to Dataverse environment variable definitions and values, including reference parsing, versioned value lookup, rotation, and 403/404 mapping to core secrets errors. Dataverse environment variable values are plaintext application configuration, not a managed secret vault; use Azure Key Vault-backed references for credentials. Web API writes emit a plaintext-storage warning unless suppressPlaintextWarning is set after an explicit risk review. The injected client constructor option remains available as a compatibility facade. |
| DataverseMetadata | Real adapter | Maps table, column, relationship, form, view, and global option-set operations to Dataverse metadata Web API routes. The injected client constructor option remains available as a compatibility facade. |
| DataverseSolutions | Real adapter | Maps solution create, component add, export, import, publish, and delete operations to Dataverse Web API table routes and actions. The injected client constructor option remains available as a compatibility facade. |
| PowerAutomateFlows | Real adapter | Maps cloud flow list, get, upsert, delete, enable/disable, and run-history operations to Dataverse workflows and processsessions Web API routes. Package import/export remains explicitly unsupported in Web API mode until validated through PAC or solution packaging. The injected client constructor option remains available as a compatibility facade. |
| DataverseGateway | Client facade | Delegates gateway route operations to the injected client. |
| DataverseObservability, PowerPlatformTelemetry, PowerPlatformGovernance, PowerPlatformSecurity, PowerPlatformCostManagement | Client facade | Delegate provider-specific operations to the injected client. Shared Azure observability, identity, and Key Vault behavior remains in @magic-alm/azure. |
| PowerAutomateAlerting | Client facade | Delegates alert operations to the injected client until concrete Power Automate mappings are added. |
| PowerPlatformAdmin, PowerPagesPortal, CanvasAppsAuthoring, PowerAppsLifecycle | Client facade | Package-local lifecycle surfaces over an injected client where core interfaces are missing or provider mappings are still unvalidated. |
| PowerPlatformCI | Unsupported | Throws UnsupportedOperationError instead of fabricating provider-neutral CI/CD behavior over incomplete PAC pipeline semantics. |
Follow-up implementation should replace client facades with focused Web API or PAC mappings. Do not update #96 completion claims to imply concrete adapter coverage until those mappings exist and live integration tests cover them.
AI Guides
Structured package guidance for AI agents and automation authors lives in docs/ai-guides/:
- Solution deployment
- Table creation
- Data operations
- Canvas apps
- Power Pages
- Error handling
- Environment safety
Examples
Runnable TypeScript examples live in examples/, including
examples/11-composite-client.ts for the provider-neutral client. They use
injected clients and command runners so they can compile without live Power
Platform credentials. Validate them with:
npm run examples:typecheck -w @magic-alm/powerplatformSafety Helpers
The package exports pure helpers for environment URL comparison, PAC org who output checks, and operation risk classification. These helpers do not shell out and do not prompt; host applications own command execution and confirmation UX.
Operation Routing
routePowerPlatformOperation and PowerPlatformOperationRouter.route() provide deterministic tool-choice guidance for package consumers. They return mcp, package-provider, webapi, or pac-cli decisions without invoking tools or making network calls.
Smart Router Quickstart (PowerPlatformScaffold)
PowerPlatformScaffold is the intent-based execution layer on top of that guidance: it resolves each operation to a concrete configured provider using the typed OPERATION_REGISTRY, capability detection, and per-operation fallback chains, then executes it and maps failures into the core error hierarchy.
It remains available for compatibility. Use PowerPlatformCompositeClient
for the provider-neutral result contract and runtime fallback behavior described
above.
import {
detectPowerPlatformCapabilities,
NodeCommandRunner,
PowerPlatformScaffold,
type PowerPlatformClient,
} from "@magic-alm/powerplatform";
const runner = new NodeCommandRunner();
// Probe providers once at startup; each probe is optional.
const capabilities = await detectPowerPlatformCapabilities({
pac: { runner },
webApi: {
baseUrl: process.env.POWERPLATFORM_ENVIRONMENT_URL!,
tokenProvider: async () => process.env.POWERPLATFORM_ACCESS_TOKEN,
},
mcp: { baseUrl: process.env.POWERPLATFORM_MCP_URL }, // omit baseUrl for in-process MCP
});
const scaffold = new PowerPlatformScaffold({
providers: {
"pac-cli": pacProvider, // PowerPlatformClient facades over PacCliIac / runPac,
webapi: webApiProvider, // PowerPlatformRestClient or host SDK clients,
mcp: mcpProvider, // and MCP tool surfaces — all host-composed.
},
capabilities,
});
// "solution.export" prefers PAC CLI and falls back to the Web API provider.
const exported = await scaffold.execute("solution.export", { name: "core" });
// forceProvider is the escape hatch: it bypasses registry preference and
// capability checks (a failed check only emits an onWarning diagnostic).
await scaffold.execute("solution.export", { name: "core" }, { forceProvider: "webapi" });Routing behavior:
- Operations must be in
OPERATION_REGISTRY(a closed, typed set) or the call throwsUnknownOperationError. Passing aspecin the execution context routes unregistered operations through theroutePowerPlatformOperationheuristic instead, androutePowerPlatformOperationChainexpands that decision into an ordered candidate chain. Before invoking any available candidate for an unregistered operation, the scaffold requires that client to advertise the operation throughsupportedOperations(); a mismatch fails immediately withProviderContractMismatchErrorrather than silently skipping across an incompatible provider boundary. - The scaffold walks
preferredthenfallbacks(providerChain(operation, context)exposes the chain it will walk), skipping providers that are unconfigured or whose capability flag isfalse. If the chain is exhausted it throwsNoAvailableProviderErrornaming the attempted providers. NodeCommandRunneris the batteries-includedCommandRunner(spawns without a shell, resolves non-zero exits intoCommandResult). Hosts can keep injecting their own runners.PowerPlatformOperationRouterremains exported and unchanged; the scaffold is a parallel API, not a replacement.
Error-context behavior:
- Registry entries carry an optional
errorContextKind(datastore,iac,secrets,gateway, orgeneric).executeandexecuteWithuse it — plus well-known input fields such astable,id,name, andpath— to build thePowerPlatformErrorContexthanded towithPowerPlatformErrorMapping, so failures surface as core domain errors instead of generic ones: adatastore.read404 becomesNotFoundError, a PAC failure onsolution.checkbecomesTemplateValidationError, andsolution.import/solution.exportfailures becomeDeploymentFailedError. - Spec-routed operations infer the kind from the spec (
solution/environmentscopes map toiac;dataversescope orrecordentity types map todatastore); everything else maps generically viaPowerPlatformProviderError. - Callers can override the derivation entirely by passing
errorContextin the execution context (or as the fourth argument toexecuteWith).derivePowerPlatformErrorContextis exported for hosts that want the same derivation outside the scaffold.
Adapter wrappers:
PacCliWrapperis a referencePowerPlatformClientover PAC CLI: it translatessolution.export,solution.import, andsolution.checkintorunPacinvocations, so hosts can register it directly as the scaffold'spac-cliprovider.McpWrapperis a referencePowerPlatformClientover an MCPtools/callsurface: it maps scaffold operation names to MCP tool names through a safe whitelist (DEFAULT_MCP_TOOL_NAMESmatchesPowerPlatformMcpTools; passtoolNamesto replace it) and refuses anything outside the map withUnknownOperationError. Successful calls require exactly one non-empty JSON text payload by default; raw text is accepted only for operations explicitly listed inresponseFormatsastext.- Both wrappers let provider failures flow through the scaffold's error-mapping boundary, so the error-context behavior above applies unchanged. See
src/__tests__/adapter-wrappers.test.tsfor end-to-end wiring examples.
CLI Strategy
Power Platform command UX should start as MAM CLI plugin commands rather than a binary inside this adapter package. See Power Platform CLI strategy for the decision record, tradeoffs, and prerequisites for future command implementation.
Verification
Run these checks before opening adapter changes:
npm test -w @magic-alm/powerplatform
npm run typecheck -w @magic-alm/powerplatform
npm run examples:typecheck -w @magic-alm/powerplatform
npm run build -w @magic-alm/powerplatformFor broad changes, also run:
npm run build
npm testLive integration tests are intentionally not part of the default suite. Gate them behind explicit credentials and target environment variables such as POWERPLATFORM_ENVIRONMENT_URL, POWERPLATFORM_ACCESS_TOKEN, POWERPLATFORM_TENANT_ID, POWERPLATFORM_CLIENT_ID, and POWERPLATFORM_CLIENT_SECRET.
The package includes an opt-in integration test harness under src/__tests__/integration/. The tests self-skip unless POWERPLATFORM_INTEGRATION=1 and all required credentials for that test are present. RUN_INTEGRATION_TESTS=true is accepted as a compatibility alias for workflow callers, but POWERPLATFORM_INTEGRATION=1 is the package-level convention. Run them only against a disposable or explicitly approved environment:
POWERPLATFORM_INTEGRATION=1 \
POWERPLATFORM_ENVIRONMENT_URL=https://your-env.crm.dynamics.com \
POWERPLATFORM_ACCESS_TOKEN=... \
npm test -- packages/powerplatformMaintainers can run the same harness from GitHub Actions with the CI workflow's manual dispatch input run_powerplatform_integration=true. Configure the powerplatform-staging environment with the required POWERPLATFORM_* secrets before dispatching the job.
