Render an accessible result card
Create a checked result model from the current result in a Model Context Protocol (MCP) application, then pass it to the renderer that fits your application. Em See Pea supplies the result structure and accessibility semantics. Your application keeps ownership of domain parsing, calculations, wording, effects, action authorization, and styles.
Before you start
Section titled “Before you start”You need Node.js 22 or newer and an application with a page shell. Continuous
integration (CI) exercises Node.js 22 and 24. The page shell
owns the document language, title, skip link, main landmark, one H1, routing,
route focus, and visible focus styles. Add an empty element with id="result"
where the card belongs.
Renderer installation comes after the shared result mapping below. Styling is a separate choice from the renderer.
Map each current result
Section titled “Map each current result”Create a new ResultView from the current checked domain result whenever the
result or relevant UI state changes. Put the mapping function in
result-view.ts beside the client or widget component that renders the card.
Runtime parsing happens before this adapter. The adapter formats already checked data for presentation; domain parsing, calculations, permissions, effect authorization, and styling stay outside it. This example declares a small domain type so it can run on its own. In an application, import the equivalent post-parse type from your domain layer.
import { defineResultView, type ResultView } from "@emseepea/server/ui";
export interface PlantingPlanResult { readonly title: string; readonly matchingCount: number; readonly notice: string;}
export function toResultView(result: PlantingPlanResult): ResultView { const count = result.matchingCount; const headline = `${count} ${ count === 1 ? "variety matches" : "varieties match" }`;
return defineResultView({ id: "pea-result", heading: result.title, headline, metrics: [{ label: "Matching varieties", value: String(count) }], reasons: { label: "Why these varieties match", items: ["They suit the selected growing conditions."], }, disclaimer: result.notice, state: { kind: count === 0 ? "empty" : "ready", status: count === 0 ? "No pea varieties match the selected conditions." : `Pea planting plan ready: ${headline}.`, focusTarget: "none", }, });}Each returned view is bounded and validated before rendering. It has a contextual heading, a definition list for the metric, a labelled reasons list, a disclaimer, and one persistent polite status message.
Choose a renderer
Section titled “Choose a renderer”Select a renderer below. Your selection applies to the installation, rendering, and lifecycle instructions on this page.
Use native HTML when your application does not need a framework renderer. Use React when your application already uses React. Use Svelte when your application already uses Svelte or you choose it for a self-contained Model Context Protocol Apps (MCP Apps) resource.
Install the renderer
Section titled “Install the renderer”Native HTML
Install the checked model and native HTML renderer.
npm install @emseepea/serverReact
Install the checked model, React renderer, React, and React DOM.
npm install @emseepea/server @emseepea/react react react-domSvelte
Install the checked model, Svelte renderer, and Svelte.
npm install @emseepea/server @emseepea/svelte svelteRender the current result
Section titled “Render the current result”Native HTML
In native.ts, use renderResultView when the application does not need
a framework renderer. Pass the heading level that fits the surrounding
page and a unique idPrefix for every result in the document.
import { renderResultView } from "@emseepea/server/ui";import { toResultView, type PlantingPlanResult,} from "./result-view.js";
const target = document.querySelector<HTMLElement>("#result");if (!target) throw new Error("Missing #result host element");
export function renderInitialPlantingPlan( result: PlantingPlanResult,): void { target.innerHTML = renderResultView(toResultView(result), { headingLevel: 2, idPrefix: "pea-result-card", });}The host element now contains native headings, lists, and status semantics. The renderer escapes result text and does not create the page shell.
React
In react.tsx, use React when the application already uses React.
import { ResultCard } from "@emseepea/react";import { toResultView, type PlantingPlanResult,} from "./result-view.js";
export function PlantingPlanCard({ result,}: { readonly result: PlantingPlanResult;}) { const view = toResultView(result);
return ( <ResultCard view={view} headingLevel={2} idPrefix="pea-result-card" /> );}The parent component passes its latest checked result as result. React
calls the adapter during each render, so the card receives a current
ResultView.
Svelte
In SvelteApp.svelte, use Svelte when the application already uses
Svelte or you choose it for a self-contained MCP Apps resource.
<script lang="ts"> import { ResultCard } from "@emseepea/svelte"; import { toResultView, type PlantingPlanResult, } from "./result-view.js";
let { result }: { result: PlantingPlanResult } = $props(); const view = $derived(toResultView(result));</script>
<ResultCard {view} headingLevel={2} idPrefix="pea-result-card" />The parent component passes its latest checked result as result.
Svelte recomputes view when that prop changes.
All three renderers produce the same names, semantics, heading level, and
status. ResultCard does not parse domain data or perform effects. Em See Pea
does not provide a Svelte initializer.
Connect MCP Apps lifecycle only when needed
Section titled “Connect MCP Apps lifecycle only when needed”Native HTML
Call renderInitialPlantingPlan once when the first checked result
arrives. For later status changes, keep the status element mounted and
update its textContent. Use React or Svelte when the rest of the card
also changes.
A native MCP Apps resource can use createMcpAppController from
@emseepea/server/ui directly. Call connect() after the document is
ready, subscribe to checked state changes, and run the cleanup function
returned by connect() during teardown.
React
Use useMcpApp only when the React card needs the MCP Apps host
lifecycle. The hook connects during mounting and disconnects during
cleanup.
Svelte
Use createMcpApp only when the Svelte card needs the MCP Apps host
lifecycle. The binding connects during mounting and disconnects when the
component is destroyed.
The lifecycle bindings handle initialization, host context, tool results,
cancellation, teardown, and ui/message through the shared checked controller.
The maintained React result-card fixture and Svelte result-card fixture show the framework bindings. Treat every host message as untrusted input.
Compare React and Svelte bundle measurements
Section titled “Compare React and Svelte bundle measurements”If React and Svelte both fit your application, the maintained equivalent
production fixture can inform your choice. It measured Svelte 27.9% smaller
with gzip and 27.2% smaller with Brotli. The fixture used esbuild 0.28.2,
React and React DOM 19.2.8, Svelte 5.57.0, minified ES modules, an ES2022
target, and no source maps.
- React: 534,121 raw bytes; 129,181 bytes with gzip level 9; 109,052 bytes with Brotli quality 11.
- Svelte: 406,619 raw bytes; 93,075 bytes with gzip level 9; 79,414 bytes with Brotli quality 11.
- Reduction: 23.9% raw; 27.9% with gzip; 27.2% with Brotli.
These measurements apply only to this fixture and dependency set. They do not
establish a general performance or bundle-size advantage. Run
npm run measure:result-cards in the repository to reproduce them.
Add the optional shared styles
Section titled “Add the optional shared styles”Renderer choice does not add styles. All three renderers are unstyled unless your application imports styles.
Install the optional stylesheet package, then import its one CSS file from your application entry point:
npm install @emseepea/tailwindimport "@emseepea/tailwind/styles.css";The same import styles native renderResultView, React ResultCard, and
Svelte ResultCard. Set data-emseepea-theme="light" or
data-emseepea-theme="dark" on an ancestor. In React, pass the checked host
theme to useMcpTheme and apply its result. Native HTML and Svelte applications
resolve and apply the checked host value themselves. The renderers do not
choose or apply that theme.
Your application still owns domain mapping, wording, business policy, actions, effect authorization, the page shell, theme application, branding, and style overrides. Removing the import changes presentation only.
Add actions and state updates
Section titled “Add actions and state updates”Result actions are native buttons. An action callback reports the selected presentation action; it does not authorize a server effect. Check identity, permissions, current state, and request data again at the effect boundary.
For an action that continues the conversation, set the state to sending
before awaiting app.sendMessage(prompt). This sends a ui/message request.
While it is pending, set action.disabled to true and update the existing
status to ongoing text such as Asking ChatGPT: Show me growing tips.
After the host accepts the message, use completion text such as
Asked ChatGPT: Show me growing tips. If the request fails, show a short error.
Tell the user how to continue in the chat.
The maintained React result example implements this sequence without making the action a server-side effect.
Update the existing state.status text for loading, completion, cancellation,
and errors. Keep one persistent polite status instead of adding another live
region. Use focusTarget only when focus must move after a meaningful update.
Do not make the whole card clickable or add custom card roles, positive
tabindex values, redundant ARIA, or extra landmarks.
Check the result
Section titled “Check the result”The documentation test suite builds these examples outside the monorepo against packed packages. The cross-renderer contract tests also check hostile text escaping, names, semantics, heading levels, status updates, focus targets, native buttons, mobile reflow, forced colours, reduced motion, and contrast-related states.
After adapting the model, run your application build and keyboard checks. Confirm that the card reflows at 320 CSS pixels, visible focus remains clear, and status changes are announced once without moving focus unnecessarily.
Source and package checks do not prove publication. PUBLISHED does not prove
that an adopter uses the package successfully. Adopter PROD_VERIFIED requires
independent evidence from the adopter’s exact production revision and Result
Card journey.
For the complete checked behaviour, read: