Build a Customer 360 workflow-backed MCP tool
This tutorial exposes three existing REST reads as one customer_360 MCP
tool. A Serverless Workflow definition runs the calls in parallel and appends
their responses into one ordered result array:
customer_360({customerId, channel})
-> light-gateway /mcp
-> light-workflow
|-- GET /customers/{customerId}
|-- GET /customers/{customerId}/preferences?channel={channel}
`-- GET /customers/{customerId}/policies
-> [{profile}, {preferences}, {policies}]
The demo reuses demo-customer-profile-api; it does not deploy an MCP wrapper
for the REST API.
Create the workflow definition and tool through the Portal UI. Do not insert
rows into wf_definition_t, tool_t, workflow_tool_binding_t, or
workflow_endpoint_target_t. Light Portal is event sourced:
Create Workflow Definition
-> WorkflowDefinitionCreatedEvent
-> wf_definition_t
Create Tool with a workflow binding and endpoints
-> ToolCreatedEvent
-> tool_t
-> workflow_tool_binding_t
-> workflow_endpoint_target_t
The workflow binding and its registered endpoint targets are part of the Create Tool command. There is no separate binding or endpoint-target Create page.
What this simplified demo covers
- REST API aggregation without an MCP wrapper service.
- Parallel orchestration with a workflow
fork. - A simple append transformation into one MCP response.
- Portal command/event creation of the workflow, tool, binding, and endpoint registrations.
- Static
mcp-router.ymlpublication for a local Light-Fabric instance.
This is an incremental demo, not the final transformer-tool implementation. The current workflow HTTP executor calls the internal demo endpoints directly and does not forward the MCP caller’s JWT. In addition, the current non-competing fork fails the invocation when any branch fails; it does not yet return the required partial response. Original-JWT propagation, gateway-routed API destinations, UI-side transformation validation, config-server publication, and partial-error append behavior remain follow-up work.
Prerequisites
- Start the
portal-config-loc/all-in-ltRust stack. - Confirm
light-gateway,light-workflow,hybrid-command,hybrid-query, PostgreSQL, anddemo-customer-profile-apiare healthy. - Sign in to Light Portal and select the target host.
- Use an account that can administer workflow definitions and GenAI tools.
- Download the versioned customer-360-mcp definition.
- Confirm that the host does not already have an active workflow named
customer-360-mcpat1.0.0or an active tool namedcustomer_360.
The included demo data uses customer CUST-1001 and channel portal.
Workflow definition
The importable YAML is maintained with this tutorial and is included below so the rendered mdBook and downloadable asset cannot drift apart:
document:
dsl: "1.0.3"
namespace: light-demo
name: customer-360-mcp
version: "1.0.0"
title: Customer 360 Workflow MCP Demo
summary: Read a customer profile, preferences, and policies in parallel and append the API responses.
tags:
demo: workflow-mcp
capability: aggregation
evaluate:
language: cel
input:
schema:
format: json
document:
type: object
additionalProperties: false
required:
- customerId
- channel
properties:
customerId:
type: string
minLength: 1
maxLength: 64
channel:
type: string
minLength: 1
maxLength: 32
output:
schema:
format: json
document:
type: object
additionalProperties: false
required:
- customerId
- results
properties:
customerId:
type: string
results:
type: array
minItems: 3
maxItems: 3
items:
type: object
additionalProperties: false
required:
- source
- status
- data
properties:
source:
type: string
status:
type: string
const: success
data:
type: object
do:
- loadCustomerContext:
fork:
branches:
- profile:
call: http
with:
method: GET
endpoint:
uri: http://demo-customer-profile-api:8085/customers/${{customerId}}
output: content
metadata:
endpointRef: customer-360.profile
- preferences:
call: http
with:
method: GET
endpoint:
uri: http://demo-customer-profile-api:8085/customers/${{customerId}}/preferences?channel=${{channel}}
output: content
metadata:
endpointRef: customer-360.preferences
- policies:
call: http
with:
method: GET
endpoint:
uri: http://demo-customer-profile-api:8085/customers/${{customerId}}/policies
output: content
metadata:
endpointRef: customer-360.policies
compete: false
export:
as:
responses: .output
- appendResponses:
set:
customerId: "${{ customerId }}"
results:
- source: profile
status: success
data: "${{ responses.profile }}"
- source: preferences
status: success
data: "${{ responses.preferences }}"
- source: policies
status: success
data: "${{ responses.policies }}"
1. Review the workflow
The workflow has one non-competing fork with three GET branches. Each HTTP
task declares metadata.endpointRef; a workflow-backed invocation is allowed
to call only an active endpoint registered by the tool binding with the same
reference and method.
After the fork joins, appendResponses produces this response shape:
{
"customerId": "CUST-1001",
"results": [
{"source": "profile", "status": "success", "data": {}},
{"source": "preferences", "status": "success", "data": {}},
{"source": "policies", "status": "success", "data": {}}
]
}
The fixed order makes the append transformation predictable even though the three calls execute concurrently.
2. Create the workflow definition
-
In the Portal sidebar, expand Workflow Admin and select Wf Definition.
-
Select Create New WfDefinition.
-
In Workflow Editor, select Import and open
customer-360-mcp.yaml. -
Confirm these values:
Field Value Namespace light-demoName customer-360-mcpVersion 1.0.0Publish in Workflow Catalog Enabled The selected host supplies Host ID. Leave Workflow Definition ID empty so the command handler generates it.
-
Select Validate and resolve any error.
-
Select Save once. Wait for Workflow definition saved.
-
Return to Wf Definition, refresh the list, and open the new row with Update. Record its generated Workflow Definition ID as
<wfDefId>.
Opening the Update page confirms that the event and projection are both available. Do not submit Create repeatedly while waiting for projection.
The normalized definition digest for the supplied file is:
sha256:a7c1c07164110840222fd2528122d75ced9f69d4e7b7b2944ea15dddd14e10ae
Changing the workflow changes this digest. If you edit the definition, compute and use the new value consistently in the tool binding and router config.
3. Create the tool, binding, and endpoint registrations
-
In the sidebar, expand GenAI Admin and select Tool.
-
Select Create Workflow-backed Tool.
-
Enter these values. Unlisted optional fields can remain empty.
Field Value Name customer_360Description Read a customer profile, preferences, and policies in parallel and append the API responses.Routing Domain CustomerSemantic Namespace customer-360Semantic Description Aggregate customer context for an agent in one read-only call.Lifecycle Status activeRead Only Enabled Idempotent Enabled Destructive Disabled Human Approval Required Disabled Version 1.0.0Execution Placement workflowWorkflow Definition light-demo/customer-360-mcp @ 1.0.0Implementation Typeis not required for a workflow-placed tool. -
Portal loads the immutable workflow definition and derives the internal definition, schema, and policy digests. These implementation fields are not entered by the user. The reviewed workflow-tool runtime profile supplies execution limits and resolves cataloged endpoint references.
-
Submit Create Tool Form once.
-
Return to Tool, refresh the list, and open
customer_360with Update Tool. Record the generated Tool ID as<toolId>and confirm:- Execution Placement is
workflow. - Stable Tool Reference equals
<toolId>. - The binding contains the selected Workflow Definition ID and a generated
bindingId. - All three endpoint entries are present.
- Execution Placement is
The command handler generates the Tool ID and binding ID. The event projector
creates all four read-model types from the one ToolCreatedEvent; do not fill
in any missing projection row manually.
The reviewed runtime profile must permit at least three parallel branches for this workflow. Confirm that limit when reviewing the generated binding; Portal does not currently validate workflow structure against runtime-profile limits.
4. Enable the static light-gateway entry
Open
portal-config-loc/all-in-lt/light-gateway-rust/config/mcp-router.yml. The
local demo configuration includes a complete customer_360 entry after the
smoke tool.
-
Set
workflowBinding.stableToolRefandworkflowBinding.workflowDefinitionIdto the IDs generated by your Portal commands. The IDs shipped in a developer checkout are valid only for the database in which those Create events were produced. -
Keep the
- name: customer_360entry aligned undertools:. -
Do not change the schemas, digests, timeouts, or budget unless you also update the Portal binding to match.
-
Confirm
light-gateway-rust/config/access-control.ymlcontainscustomer_360underskipPathPrefixesfor this local demo. -
Restart light-gateway from
portal-config-loc/all-in-lt:docker compose -f docker-compose.yml -f docker-compose-rust.yml restart light-gateway
Editing mcp-router.yml publishes runtime configuration; it does not create a
Portal entity and is not a substitute for the event-sourced steps above.
5. Verify from the desktop
Set a current access token, including the Bearer prefix. Do not copy a
token from this tutorial:
export LIGHT_PORTAL_AUTHORIZATION='Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IkFacDAyVzZqZGNxWHpxTkk2MVlaelEifQ.eyJpc3MiOiJ1cm46Y29tOm5ldHdvcmtudDpvYXV0aDI6djEiLCJhdWQiOiJ1cm46Y29tLm5ldHdvcmtudCIsInN1YiI6IjAxOTY0YjA1LTU1MzItN2M3OS04Y2RlLTE5MWRjYmQ0MjFiOCIsImV4cCI6MTc4NjYyMjU0MywianRpIjoiRkMxM0ZmYzNUMm0zLXlMT1RHc3NHQSIsImlhdCI6MTc4NjYyMTk0MywibmJmIjoxNzg2NjIxODIzLCJ2ZXIiOiIxLjAiLCJjaWQiOiJmN2Q0MjM0OC1jNjQ3LTRlZmItYTUyZC00YzU3ODc0MjFlNzIiLCJzY3AiOlsicG9ydGFsLnIiLCJwb3J0YWwudyJdLCJjbGllbnRfaWQiOiJmN2Q0MjM0OC1jNjQ3LTRlZmItYTUyZC00YzU3ODc0MjFlNzIiLCJzY29wZSI6InBvcnRhbC5yIHBvcnRhbC53IiwiY3NyZiI6IjNFS3lmUkxpVFBtTFM2QXVIMEZNcGciLCJlaWQiOiJzaDM1IiwiZW1sIjoic3RldmUuaHVAbGlnaHRhcGkubmV0IiwiaG9zdCI6IjAxOTY0YjA1LTU1MmEtN2M0Yi05MTg0LTY4NTdlN2YzZGM1ZiIsInJvbGUiOiJhY2NvdW50LW1hbmFnZXIgYWRtaW4gZ2l0aHViLXJlYWRlciBob3N0LWFkbWluIG1jcC1yZWFkZXIgdXNlciIsInVpZCI6IjAxOTY0YjA1LTU1MzItN2M3OS04Y2RlLTE5MWRjYmQ0MjFiOCIsInV0eSI6IkUifQ.D47-XR66YEdm_KLr8sgNyKxmuXeLtqjMv0h4AIhf8w5ph0T-l4Cgyc2GIP76finZjy04OguEeAfN_qqAqQu2sLb-OOTo3-WekSmmKQAX5yJLKZJup8DbShNydhTES4GklgLVltF86Cj5npJBtj8VF3Kptd67gdrZ8TF-7o9DvLjD8Umv2kz1vjijCX0J5xyjjX8HFC7cOGsHd8gKieGTZfVykTcJlZMjbzU3zlYIIjYlkmpLrQDai56eJF7wKr-CoXV3Gg3fXjUK7wsLfo1nbBLr0I20qMdXAJLYnfoXszaN3ZYGKl27W-Hh6X5iITFrTD92XavAVU24QEltjJzk7A'
List tools through light-gateway’s published HTTPS port:
curl -skS --max-time 45 \
-H "Authorization: $LIGHT_PORTAL_AUTHORIZATION" \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'MCP-Protocol-Version: 2026-07-28' \
-H 'MCP-Method: tools/list' \
--data-binary '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientInfo":{"name":"customer-360-demo","version":"1"},"io.modelcontextprotocol/clientCapabilities":{}}}}' \
https://localhost/mcp | jq
Confirm that result.tools contains customer_360, then invoke it:
curl -skS --max-time 45 \
-H "Authorization: $LIGHT_PORTAL_AUTHORIZATION" \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'MCP-Protocol-Version: 2026-07-28' \
-H 'MCP-Method: tools/call' \
-H 'MCP-Name: customer_360' \
--data-binary '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"customer_360","arguments":{"customerId":"CUST-1001","channel":"portal"},"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientInfo":{"name":"customer-360-demo","version":"1"},"io.modelcontextprotocol/clientCapabilities":{}}}}' \
https://localhost/mcp | jq
Confirm that isError is false and that the appended result contains:
- Profile:
Avery Chen. - Preferences: category
traveland channelportal. - Policy:
POL-AUTO-1001.
The request goes directly from the desktop to https://localhost/mcp; no
docker exec is required.
To run the repeatable Hurl assertions from light-portal-test against
portal-config-loc or a development light-portal-install deployment:
cd /home/steve/workspace/light-portal-test
make workflow-mcp
Troubleshooting
The Create event goes to the DLQ
Check for an older active resource with the same workflow identity or tool name. Also confirm the tool event references the active workflow ID and exact version. Repair the bad event or legacy fixture through the supported event replay/cleanup flow; do not insert or update projection rows.
The tool form rejects the binding
Confirm that:
<wfDefId>is a UUID belonging to the activecustomer-360-mcpdefinition.- Every digest starts with
sha256:followed by 64 lowercase hexadecimal characters. - The top-level and binding Schema Digest values match.
- The tool is read-only, non-destructive, and headless for synchronous use.
- Every endpoint has
endpointRef,endpointUri, and a non-emptyallowedMethodsarray. - The structured editor changes were applied before submission.
The invocation says the endpoint is not registered
Compare each workflow metadata.endpointRef with its binding endpoint entry.
The values are case-sensitive. Also verify that GET is in allowedMethods
and that the endpoint is active on the same host and binding.
The invocation says the definition or policy does not match
Keep the workflow version and all four digests identical between the Portal
binding and mcp-router.yml. Use the supplied definition without edits for
the documented digest.
The fork exceeds the runtime budget
Set maximumParallelism to at least 3 in both the Portal binding and static
router entry.
One REST call fails
The current non-competing fork fails the entire invocation. That is a known limit of this simplified demo. Do not present it as the customer’s required partial-response behavior; that needs the planned try/error-capture extension before failed branches can be appended beside successful responses.