# Two-CLT Modeling for an MCP Tool Output A custom MCP server tool backed by an Apex Invocable Action returns the platform's **invocable-action result envelope**. The real payload the tool consumer cares about lives under `outputValues`; the surrounding fields (`actionName`, `isSuccess`, `errors`, `sortOrder`, `version`, …) are envelope metadata. To render this with an HXL widget we model it as **two object-based CLTs** and wire them with a renderer that bridges the nesting. Both are ordinary CLTs of equal standing — nothing in the platform or the metaschema distinguishes an "envelope type" from a "response type." The only reason two files exist is that one CLT (the envelope) must reference the other (the response) by name via `c__`, and a CLT cannot reference itself — so the two need distinct deployed names, nothing more. Don't invent a role-label pair for the two CLTs themselves ("Payload CLT"/"Envelope CLT", "Outer CLT"/"Inner CLT") — name and describe each by what it actually models (see below), and in prose refer to them by that same identifier: "the `` envelope" / "the `Response`". ("Payload" and "response" remain fine as ordinary words for the data itself — e.g. "response fields", "the payload the tool consumer cares about" — the rule is about not naming or labeling the *CLTs* by an invented role.) ## Naming convention | Artifact | Convention | Example | |---|---|---| | Envelope CLT | `` | `getFlightDetails` | | Response CLT | `Response` | `getFlightDetailsResponse` | | Widget | `Widget` | `getFlightDetailsWidget` | - `` is derived **deterministically from the Apex class / Invocable Action name**, not the label: lower-camelCase the class name and strip a trailing `Action`, `Test`, or `WidgetAction` suffix if present. - `AccountSummaryWidgetAction` → `accountSummary` - `GetFlightDetailsAction` → `getFlightDetails` - `GetAccountSummaryTest` → `getAccountSummary` - Only when no class/action name is available at all (e.g. a bare `sample` with no `actionName` resolvable to a class) fall back to camelCasing the tool/action **label** (e.g. `Get Account Summary` → `accountSummary`). - Both CLT names are derived from the **same single ``** — there is no separate naming decision to make per artifact, and no free-standing role word (no "Result", "OutputValues", "Payload", "Envelope", and no `_CLT` suffix either — a Lightning Type is identified by living under `lightningTypes/`, not by a suffix on its name). `Response` is not a role label; it is literally what the class is (the Invocable Action's declared `List<...Response>` return-element type) — the same word the Apex source itself already uses (e.g. `GetAccountSummaryResponse`, `FlightDetailsResponse`). - The envelope CLT is *structurally* generic but **cannot be a single shared CLT** — its `outputValues` must be typed to a tool-specific response CLT via `c__`. One envelope CLT per tool. > **Naming convention supersedes the earlier hand-verified prototype.** Two bundles were manually fixed and successfully deployed during development using an older `Result` / `OutputValues` naming pair — that proved the *structural* pattern (two object-based CLTs, `@apexClassType` for nested fields, two-level renderer bindings) deploys correctly. The naming convention above replaces those two suffixes; every other structural rule in this file (root keys, `unevaluatedProperties`, the `c__` reference, nested-object typing, renderer bindings) is unchanged and still matches what was verified. ## Response CLT Object-based CLT whose `properties` are exactly the response `@InvocableVariable` fields (1:1). Root `lightning:type` is `lightning__objectType`. `platform-custom-lightning-type-generate` injects and enforces `"unevaluatedProperties": false` on every object-based CLT (its metaschema rejects a CLT without it) — this orchestrator does not fight that, it matches it in every example and every generated file. Give both CLTs a real, tool-specific `description` (never `""`) — it is what a consumer sees when picking a referenced CLT. Both CLTs also carry a root-level `"lightning:tags": ["mcp"]` — it marks the type as MCP-tool-generated per `platform-custom-lightning-type-generate/assets/primitive-types-and-constraints.md`. ```json { "title": "Get Account Summary Response", "description": "Response fields from the GetAccountSummaryTest invocable-action (GetAccountSummaryResponse)", "type": "object", "lightning:type": "lightning__objectType", "lightning:tags": ["mcp"], "unevaluatedProperties": false, "properties": { "accountName": { "title": "accountName", "lightning:type": "lightning__textType" }, "accountIndustry":{ "title": "accountIndustry","lightning:type": "lightning__textType" }, "contactCount": { "title": "contactCount", "lightning:type": "lightning__integerType" }, "totalOpportunityAmount": { "title": "totalOpportunityAmount", "lightning:type": "lightning__numberType" } } } ``` ### Nested-object payload fields (a second, additive case) The example above covers a **flat** payload — every `@InvocableVariable` field is a primitive. Some invocable responses instead have a field whose type is **itself an Apex class** (e.g. `GetFlightDetailsAction.FlightDetailsResponse.flightInfo`, typed `SearchFlightsAction.Flight`). Both shapes are in scope; pick the branch per field: - **Never** type that property as a bare `{"type":"object"}` (opaque, unrenderable, and not what deploys) and **never** inline it as a nested `lightning__objectType` (rejected by the CLT metaschema, same as the envelope↔response relationship below). - **Do** type it as `"@apexClassType/__$"` — e.g. `"lightning:type": "@apexClassType/c__SearchFlightsAction$Flight"` — exactly the Apex-backed-CLT convention `platform-custom-lightning-type-generate` already documents for `@apexClassType/namespace__ClassName$InnerClass`. - The **widget** and **renderer** then flatten through it — see the nested-binding note at the end of the "Default renderer" section below, and the full walkthrough in `examples/nested-object-source-prompt.md`. - A field typed `List` (a list of nested objects) is out of scope for the beta single-response flow — surface it in the build plan rather than emitting a schema for it, the same way a `maxOccurs > 1` scalar is surfaced. ```json { "title": "Get Flight Details Response", "description": "Response fields from the GetFlightDetailsAction invocable-action (FlightDetailsResponse)", "type": "object", "lightning:type": "lightning__objectType", "lightning:tags": ["mcp"], "unevaluatedProperties": false, "properties": { "flightInfo": { "title": "Flight Info", "lightning:type": "@apexClassType/c__SearchFlightsAction$Flight" } } } ``` ## Envelope CLT Object-based CLT that mimics the tool-result envelope. `outputValues` is typed to the response CLT via the referenced-CLT pattern `c__` — **not** inlined as a nested `lightning__objectType` (nested object typing is rejected by the CLT metaschema; see `platform-custom-lightning-type-generate`). ```json { "title": "Get Account Summary", "description": "Invocable-action result envelope for the GetAccountSummaryTest MCP tool", "type": "object", "lightning:type": "lightning__objectType", "lightning:tags": ["mcp"], "unevaluatedProperties": false, "properties": { "actionName": { "title": "actionName", "lightning:type": "lightning__textType" }, "isSuccess": { "title": "isSuccess", "lightning:type": "lightning__booleanType" }, "outputValues": { "title": "outputValues", "lightning:type": "c__getAccountSummaryResponse" } } } ``` - The `c__` string is the referenced type's **registered identifier / FQN**, not its `title`. It must match the response CLT's deployed name. - The response CLT must be deployed **before** the envelope CLT. - Include only the envelope scalars the widget or the platform needs (`actionName`, `isSuccess`, and `outputValues` at minimum). Add `message` etc. only when rendered. ## Default renderer (in the ENVELOPE CLT) The renderer is the **default `renderer.json` at the envelope CLT bundle root**, parallel to `schema.json` — NOT under `lightningDesktopGenAi/`. It assigns the widget and bridges the envelope nesting to the flat widget schema. ```json { "renderer": { "componentOverrides": { "$": { "definition": "@widget/c/accountSummaryWidget", "attributes": { "accountName": "{!$attrs.outputValues.accountName}", "accountIndustry":"{!$attrs.outputValues.accountIndustry}", "contactCount": "{!$attrs.outputValues.contactCount}", "totalOpportunityAmount": "{!$attrs.outputValues.totalOpportunityAmount}" } } } } } ``` **The binding path is the crux:** each widget attribute (left, flat) maps to the response field nested under the envelope's `outputValues` node (right) via `{!$attrs.outputValues.}`. Because the envelope CLT's `outputValues` is typed to the response CLT, the runtime can resolve `outputValues.` against the response CLT's `properties`. **Nested-object response fields bind one level deeper.** When a response field is itself an Apex-class reference (the `flightInfo` case above), the widget flattens to that class's own leaf fields, and the renderer binding goes two levels deep: `{!$attrs.outputValues.flightInfo.flightId}`, `{!$attrs.outputValues.flightInfo.origin}`, etc. — `outputValues..`, not `outputValues.` alone (which would bind the widget to an unresolvable object, not a renderable leaf). ## Why two CLTs and not one The platform binds the tool's output rendition to the CLT whose shape matches the **tool output schema** — that is the envelope, not the response. So the renderer (and thus the widget assignment) must live in the envelope CLT. But the widget wants a flat attribute contract, so the renderer flattens the nesting via `outputValues.`. The response CLT exists purely to *type* the `outputValues` node so those nested paths resolve. Inlining the response fields as a nested `lightning__objectType` inside the envelope is rejected by the CLT metaschema — hence a separate, referenced response CLT. ## The widget schema is generic, not CLT-derived The widget schema is a standalone contract: `properties.attributes.properties` built from the **response field list** (name + primitive `lightning:type`), nothing more. It happens to share the field set with the response CLT for this flow, but it is not typed against the CLT, does not carry `unevaluatedProperties`, and would look identical if the same field list arrived from any other source `platform-widget-generate` supports. The widget schema and body know nothing about the envelope. The widget binds `{!$attrs.accountName}` (flat); only the renderer knows the field actually lives at `outputValues.accountName`. This keeps the widget reusable and lets `platform-widget-generate` author it exactly as it would for any flat response.