MCP UI actions — how the chat drives the cart flow
One of the most-misunderstood capabilities of Nortinia AI Chat is UI control. "The chat clicks for me?" — yes, but not how you think. The chat does not move an invisible cursor. The chat invokes tools that the frontend registers via the useRegisterUIActions hook. Every call is preview-and-confirm.
The six cart-flow tools
In the cart + checkout flow, six MCP tools are registered per page state:
cart.removeItem— remove a line from the cart. Param:lineId. Preview: "I will remove from the cart: Pro license (1 pc, 49€). Confirm?".cart.applyCoupon— apply a coupon code. Param:code. Preview: "I will apply SUMMER25 (–25% on the cart total). Confirm?".cart.proceedToCheckout— navigate to the checkout page. No params. Preview: "I will take you to the checkout page. Continue?".checkout.fillField— fill a field. Params:fieldId,value. Preview: "I will fill the email field with: anna@buyer.com. Confirm?".checkout.toggleConsent— toggle a consent checkbox (ToS, GDPR). Param:consentId. Preview: "I will toggle the ToS-acceptance checkbox. Confirm?". Consent can only be activated after explicit user confirmation, never implicitly.checkout.submitQuote— in quote-only checkout, submit the RFQ. No params (the fields were already filled by the user). Preview: "I will submit the quote request. Confirm?".
Every tool is registered on the frontend. The chat backend only invokes them. The panel renders the preview, and only after the user's explicit "yes" / OK click does the action fire.
The XSS class we audited
Tool parameters echo back into the UI (in the preview text, in dashboard events). That is a classic XSS vector: if the model returns <script>alert(1)</script> as a value and the panel naively renders it into HTML, the tenant's security is broken.
Two defense layers run:
- Schema validation — every tool input passes a Zod schema.
cart.applyCoupon.codefor example:z.string().min(1).max(64).regex(/^[A-Z0-9_-]+$/).<script>simply never gets in. - Preview render pipeline — the panel renders tool params as plain text nodes (
textContent, neverinnerHTML). Even if something slipped past schema, the browser would not parse it as HTML.
Two pen-test rounds (Jan 2026, Mar 2026) found zero vulnerabilities in the chat-tool surface. These two layers hold.
The per-tenant tool registry
Not every tenant runs all six tools. cart.applyCoupon, for example, makes no sense on a quote-request tenant (e.g. the nortinia.com /tanacsadas flow) — there are no coupons there. The registry is tenant-scoped:
// per-tenant tool registry example
useRegisterUIActions([
cartRemoveItem,
cartProceedToCheckout,
checkoutFillField,
checkoutToggleConsent,
checkoutSubmitQuote,
// cartApplyCoupon intentionally omitted — quote-only cart
]);
The backend uses the tenant config to decide which tools land in the model's prompt. The model cannot invoke a tool the frontend has not registered — doubly guarded by tenant logic.
What this looks like in practice
An average cart-flow session looks like this:
- User: "Remove the Starter and apply a coupon to the cart."
- Chat: "I will remove the Starter license line (1 pc, 19€). Confirm?"
- User: ✅
- Chat: "There is an active SUMMER25 coupon (–25%). Should I apply it?"
- User: ✅
- Chat: "Done. New total: 73.50€. Should we go to checkout?"
- User: ✅ →
cart.proceedToCheckoutfires.
Four chat messages, three clicks, cart-to-checkout flow completed. Traditionally that is 6-8 navigation + form-field click steps.
Lesson
MCP UI actions are not magic. They are a disciplined contract between chat and frontend, locked into code, defended by schema, and configurable at the tenant level. The magic is that the user feels it as simple.