This document covers how Power Platform ToolBox exposes tools to external automation agents through the built-in MCP server.
External automation agents can invoke selected PPTB tools through the MCP server. PPTB supports windowed execution and now includes a headless execution contract for unattended flows.
To expose a tool to MCP, add a top-level agents section in pptb.config.json.
{
"invocation": {
"version": "1.0.0",
"capabilities": ["entity-picker"],
"prefill": {
"properties": {
"entityName": { "type": "string" },
"allowMultiSelect": { "type": "boolean" }
}
},
"returnTopic": {
"properties": {
"selectedId": { "type": "string" },
"selectedName": { "type": "string" }
}
}
},
"agents": {
"version": "1.0.0",
"invokable": true,
"modes": ["one-way", "two-way"],
"defaultMode": "two-way",
"timeoutMS": 12000
}
}Field summary:
agents.version: required semantic version for the agent contract.agents.invokable: required to expose the tool to MCP.agents.modes: supported invocation modes.agents.defaultMode: fallback mode when the agent does not specify one.agents.timeoutMS: timeout hint for two-way calls.
MCP callers can pass PPTB-specific invocation metadata under arguments.__pptb.
{
"entityName": "account",
"__pptb": {
"mode": "two-way",
"timeoutMs": 60000
}
}Supported metadata:
mode:"one-way"or"two-way".executionMode:"windowed"or"headless".timeoutMs: positive number in milliseconds.authToken: optional caller-provided token for headless execution contexts.connectionName: optional saved PPTB connection name. MCP resolves and refreshes tokens server-side when possible.
two-way: MCP waits for the callee to callreturnData(...)and validates the result againstreturnTopic.one-way: MCP returns an immediate accepted response and does not wait for a payload from the callee.
windowed: existing BrowserView launch behavior.headless: MCP returns a job acknowledgement withjobIdandjobStatusPath.
For headless + invocation mode:
headless+two-way: MCP waits for job completion and returns the final tool payload directly (schema-aligned) when successful.headless+one-way: MCP returns immediate acceptance metadata (jobId,statusUrl) and callers poll status endpoints as needed.
Headless runtime contract:
- Tool package must expose
invokeHeadless(input, context)from a discovered entry file. - Entry discovery order:
agents.headlessEntryinpptb.config.jsondist/headless.jsheadless.jspackage.json.main
invokeHeadlessmust return a JSON object payload matchingreturnTopicwhen two-way semantics are expected.
Example headless acceptance payload:
{
"status": "accepted",
"executionMode": "headless",
"mode": "two-way",
"jobId": "f5a79f96-3d4f-4f60-a151-4d2d3069f2ef",
"jobStatusPath": "/mcp/jobs/f5a79f96-3d4f-4f60-a151-4d2d3069f2ef",
"statusUrl": "/mcp/jobs/f5a79f96-3d4f-4f60-a151-4d2d3069f2ef/status",
"jobStatus": "pending",
"timeoutMs": 120000,
"hasAuthToken": true,
"authSource": "provided-token",
"connectionName": "optional-when-using-connection-name"
}Job status endpoint:
GET /mcp/jobs/{jobId}orGET /mcp/jobs/{jobId}/status(requiresX-MCP-Auth-Token)- Returns job state (
pending,in_progress,completed,failed) and result/error fields when available.
Headless context shape passed to invokeHeadless:
{
"toolId": "tool-id",
"toolName": "friendly-name",
"invocationMode": "one-way|two-way",
"authToken": "optional-token",
"updateProgress": "function(percent, message)",
"logger": {
"info": "function(message)",
"error": "function(message)"
}
}Headless tools run under Node.js, so they do not get a real browser DOM. Tool code should therefore avoid assuming that document exists, but it may still use PPTB APIs through a compatibility global installed by the headless runtime.
This is additive compatibility, not a rename. Existing browser-based tools can continue to use window.toolboxAPI, window.dataverseAPI, and window.powerplatformAPI unchanged.
The three PPTB globals are available on globalThis in headless mode, matching the same names used in browser-based tools:
| Global | Headless | Windowed | Purpose |
|---|---|---|---|
toolboxAPI |
globalThis.toolboxAPI |
window.toolboxAPI |
Connections, utils, settings, terminal, events, invocation |
dataverseAPI |
globalThis.dataverseAPI |
window.dataverseAPI |
All Dataverse CRUD, FetchXML, metadata, execute |
powerplatformAPI |
globalThis.powerplatformAPI |
window.powerplatformAPI |
Power Platform admin endpoints |
For headless tools that need Dataverse or Power Platform access, the MCP invocation should also carry a connectionName so the runtime can resolve the correct saved connection before the tool runs.
Example headless entry that matches the public API surface:
export async function invokeHeadless(input: Record<string, unknown>, context: { toolName: string; invocationMode: "one-way" | "two-way" }) {
// toolboxAPI – connections, utils, settings, terminal, events, invocation
const connection = await toolboxAPI.connections.getActiveConnection();
// dataverseAPI – exposed separately, same as window.dataverseAPI in windowed mode
const topAccounts = await dataverseAPI.queryData("accounts?$select=name&$top=5");
return {
toolName: context.toolName,
invocationMode: context.invocationMode,
connectionName: connection?.name ?? null,
rows: topAccounts.value ?? [],
};
}The globals are installed directly on globalThis, so tool code written for windowed execution (window.dataverseAPI.queryData(...)) continues to work unchanged because window is aliased to globalThis by the headless runtime.
Headless invocations now capture tool messages into the live job record so the MCP Server page can drill into an invocation and show its runtime output.
The supported logging surface is the context.logger object passed to invokeHeadless(...):
export async function invokeHeadless(
input: Record<string, unknown>,
context: { logger: { debug(message: string): void; info(message: string): void; warn(message: string): void; error(message: string): void } },
) {
context.logger.info("Starting headless work");
context.logger.warn("Using fallback data source");
context.logger.error("Something failed");
}These messages are stored with the job, surfaced in the MCP Server detail pane, and kept bounded so the log history does not grow without limit.
When a tool is launched by MCP, toolboxAPI.invocation.getLaunchContext() returns the prefill data. If present, invocation metadata is attached under __pptb.
{
"entityName": "account",
"__pptb": {
"source": "mcp",
"mode": "two-way",
"correlationId": "mcp-...",
"timeoutMs": 60000,
"expectsResponse": true
}
}Current implementation points:
- MCP server entry point: src/main/mcp/mcpServer.ts
- Agent tool registry: src/main/mcp/agentToolRegistry.ts
- Invocation context bridge: src/main/toolPreloadBridge.ts
- Logging: src/main/mcp/agentInvocationLogger.ts
The runtime currently:
- Validates agent eligibility from
agents. - Validates input against the tool's
prefillschema. - Validates output against
returnTopicfor two-way calls. - Logs mode, correlation id, and result outcome.
- Run
pnpm run buildafter changing MCP-related TypeScript or docs. - If a tool does not appear in MCP discovery, confirm
agents.invokableistrue. - If a call fails schema validation, compare the payload against
prefillorreturnTopic. - If a call times out, adjust
agents.timeoutMSor the per-call__pptb.timeoutMshint. - Use MCP Inspector as the manual test harness for this feature area.
- Verify that
list-toolsshows the supported modes and thatcall-toolreturns the expected one-way or two-way response. - Check that invocation logs redact connection-related identifiers and sensitive payload fields.
- In the PPTB renderer, the MCP Server page now refreshes live and supports drilling into a selected invocation to inspect the captured job logs, progress, result, and error state.