Skip to content

Latest commit

 

History

History
263 lines (204 loc) · 11.3 KB

MCP_IMPLEMENTATION.md

File metadata and controls

263 lines (204 loc) · 11.3 KB

MCP Implementation

This document covers how Power Platform ToolBox exposes tools to external automation agents through the built-in MCP server.

Table of Contents

Overview

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.

Agent Contract

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 Invocation Envelope

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.
  • connectionNames: saved PPTB connection names in slot order. Index 0 is primary, 1 is secondary, and 2+ addresses additional slots. Names are matched case-insensitively after trimming; unknown or ambiguous names are rejected.
  • connectionName: legacy slot-0 alias. When both fields are supplied, it must match connectionNames[0]; the array is authoritative (including an empty array).

For example, a tool declaring features.connections: { "min": 2, "max": 3 } can be invoked with:

{
    "__pptb": {
        "executionMode": "headless",
        "mode": "two-way",
        "connectionNames": ["Development", "Test", "Production"]
    }
}

The server rejects more names than the declared maximum and any names supplied to a zero-connection tool. Headless calls must supply at least the declared minimum; they do not open a connection picker or initiate fresh interactive sign-in. Authenticate interactive connections in PPTB first; cached tokens, silent refresh, client-secret and username/password authentication remain supported. authToken overrides the primary token in the invokeHeadless callback context only. Global Dataverse/Power Platform APIs use the saved connection's credentials, which are authenticated independently. A callback token does not identify a saved connection, satisfy a missing required slot, or replace saved credentials. Windowed calls can prompt for missing required slots.

Headless tools use toolboxAPI.connections.getConnections() or getConnection(2) to access later slots, and pass the same zero-based numeric target to Dataverse or Power Platform APIs. Legacy "primary" / "secondary" targets remain supported. Metadata returned by getToolContext() contains slot IDs/names/URLs, not tokens.

Mode Semantics

  • two-way: MCP waits for the callee to call returnData(...) and validates the result against returnTopic.
  • one-way: MCP returns an immediate accepted response and does not wait for a payload from the callee.

Execution Mode Semantics

  • windowed: existing BrowserView launch behavior.
  • headless: MCP returns a job acknowledgement with jobId and jobStatusPath.

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:
    1. agents.headlessEntry in pptb.config.json
    2. dist/headless.js
    3. headless.js
    4. package.json.main
  • invokeHeadless must return a JSON object payload matching returnTopic when 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} or GET /mcp/jobs/{jobId}/status (requires X-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 API Access

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 Tool Logs

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.

Tool Runtime Context

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
    }
}

Implementation Notes

Current implementation points:

The runtime currently:

  • Validates agent eligibility from agents.
  • Validates input against the tool's prefill schema.
  • Validates output against returnTopic for two-way calls.
  • Logs mode, correlation id, and result outcome.

Validation and Troubleshooting

  • Run pnpm run build after changing MCP-related TypeScript or docs.
  • If a tool does not appear in MCP discovery, confirm agents.invokable is true.
  • If a call fails schema validation, compare the payload against prefill or returnTopic.
  • If a call times out, adjust agents.timeoutMS or the per-call __pptb.timeoutMs hint.
  • Use MCP Inspector as the manual test harness for this feature area.
  • Verify that list-tools shows the supported modes and that call-tool returns 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.