Merge pull request #381 from jonathanhefner/improve-patterns-guide

Improve patterns guide with concrete examples and safe area insets
This commit is contained in:
Jonathan Hefner 2026-01-27 17:43:31 -06:00 • committed by GitHub
commit 782756c833
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 215 additions and 136 deletions

View file

@ -158,7 +158,7 @@ loadDataInChunks(resourceId, (loaded, total) => {
_See [`examples/pdf-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/pdf-server) for a full implementation of this pattern._ _See [`examples/pdf-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/pdf-server) for a full implementation of this pattern._
## Giving errors back to model ## Giving errors back to the model
**Server-side**: Tool handler validates inputs and returns `{ isError: true, content: [...] }`. The model receives this error through the normal tool call response. **Server-side**: Tool handler validates inputs and returns `{ isError: true, content: [...] }`. The model receives this error through the normal tool call response.
@ -182,18 +182,19 @@ try {
} }
``` ```
## Matching host styling (CSS variables, theme, and fonts) ## Adapting to host context (theme, styling, fonts, and safe areas)
Use the SDK's style helpers to apply host styling, then reference them in your CSS: The host provides context about its environment via {@link types!McpUiHostContext `McpUiHostContext`}. Use this to adapt your app's appearance and layout:
- **CSS variables** — Use `var(--color-background-primary)`, etc. in your CSS
- **Theme** — Use `[data-theme="dark"]` selectors or `light-dark()` function for theme-aware styles - **Theme** — Use `[data-theme="dark"]` selectors or `light-dark()` function for theme-aware styles
- **CSS variables** — Use `var(--color-background-primary)`, etc. in your CSS (see {@link types!McpUiStyleVariableKey `McpUiStyleVariableKey`} for a full list)
- **Fonts** — Use `var(--font-sans)` or `var(--font-mono)` with fallbacks (e.g., `font-family: var(--font-sans, system-ui, sans-serif)`) - **Fonts** — Use `var(--font-sans)` or `var(--font-mono)` with fallbacks (e.g., `font-family: var(--font-sans, system-ui, sans-serif)`)
- **Safe area insets** — Apply padding to avoid device notches, rounded corners, or system UI overlays
**Vanilla JS:** **Vanilla JS:**
<!-- prettier-ignore --> <!-- prettier-ignore -->
```tsx source="./patterns.tsx#hostStylingVanillaJs" ```tsx source="./patterns.tsx#hostContextVanillaJs"
function applyHostContext(ctx: McpUiHostContext) { function applyHostContext(ctx: McpUiHostContext) {
if (ctx.theme) { if (ctx.theme) {
applyDocumentTheme(ctx.theme); applyDocumentTheme(ctx.theme);
@ -204,12 +205,18 @@ function applyHostContext(ctx: McpUiHostContext) {
if (ctx.styles?.css?.fonts) { if (ctx.styles?.css?.fonts) {
applyHostFonts(ctx.styles.css.fonts); applyHostFonts(ctx.styles.css.fonts);
} }
if (ctx.safeAreaInsets) {
mainEl.style.paddingTop = `${ctx.safeAreaInsets.top}px`;
mainEl.style.paddingRight = `${ctx.safeAreaInsets.right}px`;
mainEl.style.paddingBottom = `${ctx.safeAreaInsets.bottom}px`;
mainEl.style.paddingLeft = `${ctx.safeAreaInsets.left}px`;
}
} }
// Apply when host context changes // Apply when host context changes
app.onhostcontextchanged = applyHostContext; app.onhostcontextchanged = applyHostContext;
// Apply initial styles after connecting // Apply initial context after connecting
app.connect().then(() => { app.connect().then(() => {
const ctx = app.getHostContext(); const ctx = app.getHostContext();
if (ctx) { if (ctx) {
@ -221,25 +228,52 @@ app.connect().then(() => {
**React:** **React:**
<!-- prettier-ignore --> <!-- prettier-ignore -->
```tsx source="./patterns.tsx#hostStylingReact" ```tsx source="./patterns.tsx#hostContextReact"
function MyApp() { function MyApp() {
const [hostContext, setHostContext] = useState<McpUiHostContext>();
const { app } = useApp({ const { app } = useApp({
appInfo: { name: "MyApp", version: "1.0.0" }, appInfo: { name: "MyApp", version: "1.0.0" },
capabilities: {}, capabilities: {},
onAppCreated: (app) => {
app.onhostcontextchanged = (ctx) => {
setHostContext((prev) => ({ ...prev, ...ctx }));
};
},
}); });
// Apply all host styles (variables, theme, fonts) // Set initial host context after connection
useHostStyles(app, app?.getHostContext()); useEffect(() => {
if (app) {
setHostContext(app.getHostContext());
}
}, [app]);
// Apply styles when host context changes
useEffect(() => {
if (hostContext?.theme) {
applyDocumentTheme(hostContext.theme);
}
if (hostContext?.styles?.variables) {
applyHostStyleVariables(hostContext.styles.variables);
}
if (hostContext?.styles?.css?.fonts) {
applyHostFonts(hostContext.styles.css.fonts);
}
}, [hostContext]);
return ( return (
<div <div
style={{ style={{
background: "var(--color-background-primary)", background: "var(--color-background-primary)",
fontFamily: "var(--font-sans)", fontFamily: "var(--font-sans)",
paddingTop: hostContext?.safeAreaInsets?.top,
paddingRight: hostContext?.safeAreaInsets?.right,
paddingBottom: hostContext?.safeAreaInsets?.bottom,
paddingLeft: hostContext?.safeAreaInsets?.left,
}} }}
> >
<p>Styled with host CSS variables and fonts</p> Styled with host CSS variables, fonts, and safe area insets
<p className="theme-aware">Uses [data-theme] selectors</p>
</div> </div>
); );
} }
@ -247,17 +281,18 @@ function MyApp() {
_See [`examples/basic-server-vanillajs/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-vanillajs) and [`examples/basic-server-react/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-react) for full implementations of this pattern._ _See [`examples/basic-server-vanillajs/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-vanillajs) and [`examples/basic-server-react/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-react) for full implementations of this pattern._
## Entering / Exiting fullscreen ## Entering / exiting fullscreen
Toggle fullscreen mode by calling {@link app!App.requestDisplayMode `requestDisplayMode`}: Toggle fullscreen mode by calling {@link app!App.requestDisplayMode `requestDisplayMode`}:
<!-- prettier-ignore --> <!-- prettier-ignore -->
```ts source="../src/app.examples.ts#App_requestDisplayMode_toggle" ```ts source="../src/app.examples.ts#App_requestDisplayMode_toggle"
const container = document.getElementById("main")!;
const ctx = app.getHostContext(); const ctx = app.getHostContext();
if (ctx?.availableDisplayModes?.includes("fullscreen")) { const newMode = ctx?.displayMode === "inline" ? "fullscreen" : "inline";
const target = ctx.displayMode === "fullscreen" ? "inline" : "fullscreen"; if (ctx?.availableDisplayModes?.includes(newMode)) {
const result = await app.requestDisplayMode({ mode: target }); const result = await app.requestDisplayMode({ mode: newMode });
console.log("Now in:", result.mode); container.classList.toggle("fullscreen", result.mode === "fullscreen");
} }
``` ```
@ -265,17 +300,38 @@ Listen for display mode changes via {@link app!App.onhostcontextchanged `onhostc
<!-- prettier-ignore --> <!-- prettier-ignore -->
```ts source="../src/app.examples.ts#App_onhostcontextchanged_respondToDisplayMode" ```ts source="../src/app.examples.ts#App_onhostcontextchanged_respondToDisplayMode"
app.onhostcontextchanged = (params) => { app.onhostcontextchanged = (ctx) => {
if (params.displayMode) { // Adjust to current display mode
const isFullscreen = params.displayMode === "fullscreen"; if (ctx.displayMode) {
document.body.classList.toggle("fullscreen", isFullscreen); const container = document.getElementById("main")!;
const isFullscreen = ctx.displayMode === "fullscreen";
container.classList.toggle("fullscreen", isFullscreen);
}
// Adjust display mode controls
if (ctx.availableDisplayModes) {
const fullscreenBtn = document.getElementById("fullscreen-btn")!;
const canFullscreen = ctx.availableDisplayModes.includes("fullscreen");
fullscreenBtn.style.display = canFullscreen ? "block" : "none";
} }
}; };
``` ```
In fullscreen mode, remove the container's border radius so content extends to the viewport edges:
```css
#main {
border-radius: var(--border-radius-lg);
&.fullscreen {
border-radius: 0;
}
}
```
_See [`examples/shadertoy-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/shadertoy-server) for a full implementation of this pattern._ _See [`examples/shadertoy-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/shadertoy-server) for a full implementation of this pattern._
## Passing contextual information from the App to the Model ## Passing contextual information from the App to the model
Use {@link app!App.updateModelContext `updateModelContext`} to keep the model informed about what the user is viewing or interacting with. Structure the content with YAML frontmatter for easy parsing: Use {@link app!App.updateModelContext `updateModelContext`} to keep the model informed about what the user is viewing or interacting with. Structure the content with YAML frontmatter for easy parsing:
@ -389,9 +445,9 @@ app.ontoolresult = (result) => {
_See [`examples/map-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/map-server) for a full implementation of this pattern._ _See [`examples/map-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/map-server) for a full implementation of this pattern._
## Pausing computation-heavy views when out of view ## Pausing computation-heavy views when offscreen
Views with animations, WebGL rendering, or polling can consume significant CPU/GPU even when scrolled out of view. Use [`IntersectionObserver`](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API) to pause expensive operations when the view isn't visible: Views with animations, WebGL rendering, or polling can consume significant CPU/GPU even when scrolled offscreen. Use [`IntersectionObserver`](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API) to pause expensive operations when the view isn't visible:
<!-- prettier-ignore --> <!-- prettier-ignore -->
```tsx source="./patterns.tsx#visibilityBasedPause" ```tsx source="./patterns.tsx#visibilityBasedPause"
@ -399,9 +455,9 @@ Views with animations, WebGL rendering, or polling can consume significant CPU/G
const observer = new IntersectionObserver((entries) => { const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => { entries.forEach((entry) => {
if (entry.isIntersecting) { if (entry.isIntersecting) {
animation.play(); animation.play(); // or startPolling(), etc
} else { } else {
animation.pause(); animation.pause(); // or stopPolling(), etc
} }
}); });
}); });
@ -419,31 +475,24 @@ _See [`examples/shadertoy-server/`](https://github.com/modelcontextprotocol/ext-
## Lowering perceived latency ## Lowering perceived latency
Use {@link app!App.ontoolinputpartial `ontoolinputpartial`} to receive streaming tool arguments as they arrive, allowing you to show a loading preview before the complete input is available. Use {@link app!App.ontoolinputpartial `ontoolinputpartial`} to receive streaming tool arguments as they arrive. This lets you show a loading preview before the complete input is available, such as streaming code into a `<pre>` tag before executing it, partially rendering a table as data arrives, or incrementally populating a chart.
<!-- prettier-ignore --> <!-- prettier-ignore -->
```ts source="../src/app.examples.ts#App_ontoolinputpartial_progressiveRendering" ```ts source="../src/app.examples.ts#App_ontoolinputpartial_progressiveRendering"
let toolInputs: Record<string, unknown> | null = null; const codePreview = document.querySelector<HTMLPreElement>("#code-preview")!;
let toolInputsPartial: Record<string, unknown> | null = null; const canvas = document.querySelector<HTMLCanvasElement>("#canvas")!;
app.ontoolinputpartial = (params) => { app.ontoolinputpartial = (params) => {
toolInputsPartial = params.arguments as Record<string, unknown>; codePreview.textContent = (params.arguments?.code as string) ?? "";
render(); codePreview.style.display = "block";
canvas.style.display = "none";
}; };
app.ontoolinput = (params) => { app.ontoolinput = (params) => {
toolInputs = params.arguments as Record<string, unknown>; codePreview.style.display = "none";
toolInputsPartial = null; canvas.style.display = "block";
render(); render(params.arguments?.code as string);
}; };
function render() {
if (toolInputs) {
renderFinalUI(toolInputs);
} else {
renderLoadingUI(toolInputsPartial); // e.g., shimmer with partial preview
}
}
``` ```
> [!IMPORTANT] > [!IMPORTANT]

View file

@ -16,7 +16,8 @@ import {
import { randomUUID } from "node:crypto"; import { randomUUID } from "node:crypto";
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import type { McpUiHostContext } from "../src/types.js"; import type { McpUiHostContext } from "../src/types.js";
import { useApp, useHostStyles } from "../src/react/index.js"; import { useEffect, useState } from "react";
import { useApp } from "../src/react/index.js";
import { registerAppTool } from "../src/server/index.js"; import { registerAppTool } from "../src/server/index.js";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod"; import { z } from "zod";
@ -147,10 +148,10 @@ function chunkedDataClient(app: App, resourceId: string) {
} }
/** /**
* Example: Unified host styling (theme, CSS variables, fonts) * Example: Adapting to host context (theme, CSS variables, fonts, safe areas)
*/ */
function hostStylingVanillaJs(app: App) { function hostContextVanillaJs(app: App, mainEl: HTMLElement) {
//#region hostStylingVanillaJs //#region hostContextVanillaJs
function applyHostContext(ctx: McpUiHostContext) { function applyHostContext(ctx: McpUiHostContext) {
if (ctx.theme) { if (ctx.theme) {
applyDocumentTheme(ctx.theme); applyDocumentTheme(ctx.theme);
@ -161,48 +162,81 @@ function hostStylingVanillaJs(app: App) {
if (ctx.styles?.css?.fonts) { if (ctx.styles?.css?.fonts) {
applyHostFonts(ctx.styles.css.fonts); applyHostFonts(ctx.styles.css.fonts);
} }
if (ctx.safeAreaInsets) {
mainEl.style.paddingTop = `${ctx.safeAreaInsets.top}px`;
mainEl.style.paddingRight = `${ctx.safeAreaInsets.right}px`;
mainEl.style.paddingBottom = `${ctx.safeAreaInsets.bottom}px`;
mainEl.style.paddingLeft = `${ctx.safeAreaInsets.left}px`;
}
} }
// Apply when host context changes // Apply when host context changes
app.onhostcontextchanged = applyHostContext; app.onhostcontextchanged = applyHostContext;
// Apply initial styles after connecting // Apply initial context after connecting
app.connect().then(() => { app.connect().then(() => {
const ctx = app.getHostContext(); const ctx = app.getHostContext();
if (ctx) { if (ctx) {
applyHostContext(ctx); applyHostContext(ctx);
} }
}); });
//#endregion hostStylingVanillaJs //#endregion hostContextVanillaJs
} }
/** /**
* Example: Host styling with React (CSS variables, theme, fonts) * Example: Adapting to host context with React (CSS variables, theme, fonts, safe areas)
*/ */
function hostStylingReact() { function hostContextReact() {
//#region hostStylingReact //#region hostContextReact
function MyApp() { function MyApp() {
const [hostContext, setHostContext] = useState<McpUiHostContext>();
const { app } = useApp({ const { app } = useApp({
appInfo: { name: "MyApp", version: "1.0.0" }, appInfo: { name: "MyApp", version: "1.0.0" },
capabilities: {}, capabilities: {},
onAppCreated: (app) => {
app.onhostcontextchanged = (ctx) => {
setHostContext((prev) => ({ ...prev, ...ctx }));
};
},
}); });
// Apply all host styles (variables, theme, fonts) // Set initial host context after connection
useHostStyles(app, app?.getHostContext()); useEffect(() => {
if (app) {
setHostContext(app.getHostContext());
}
}, [app]);
// Apply styles when host context changes
useEffect(() => {
if (hostContext?.theme) {
applyDocumentTheme(hostContext.theme);
}
if (hostContext?.styles?.variables) {
applyHostStyleVariables(hostContext.styles.variables);
}
if (hostContext?.styles?.css?.fonts) {
applyHostFonts(hostContext.styles.css.fonts);
}
}, [hostContext]);
return ( return (
<div <div
style={{ style={{
background: "var(--color-background-primary)", background: "var(--color-background-primary)",
fontFamily: "var(--font-sans)", fontFamily: "var(--font-sans)",
paddingTop: hostContext?.safeAreaInsets?.top,
paddingRight: hostContext?.safeAreaInsets?.right,
paddingBottom: hostContext?.safeAreaInsets?.bottom,
paddingLeft: hostContext?.safeAreaInsets?.left,
}} }}
> >
<p>Styled with host CSS variables and fonts</p> Styled with host CSS variables, fonts, and safe area insets
<p className="theme-aware">Uses [data-theme] selectors</p>
</div> </div>
); );
} }
//#endregion hostStylingReact //#endregion hostContextReact
} }
/** /**
@ -284,9 +318,9 @@ function visibilityBasedPause(
const observer = new IntersectionObserver((entries) => { const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => { entries.forEach((entry) => {
if (entry.isIntersecting) { if (entry.isIntersecting) {
animation.play(); animation.play(); // or startPolling(), etc
} else { } else {
animation.pause(); animation.pause(); // or stopPolling(), etc
} }
}); });
}); });
@ -304,8 +338,8 @@ function visibilityBasedPause(
// Suppress unused variable warnings // Suppress unused variable warnings
void chunkedDataServer; void chunkedDataServer;
void chunkedDataClient; void chunkedDataClient;
void hostStylingVanillaJs; void hostContextVanillaJs;
void hostStylingReact; void hostContextReact;
void persistViewStateServer; void persistViewStateServer;
void persistViewState; void persistViewState;
void visibilityBasedPause; void visibilityBasedPause;

View file

@ -145,33 +145,25 @@ async function App_ontoolinput_setter(app: App) {
*/ */
function App_ontoolinputpartial_progressiveRendering(app: App) { function App_ontoolinputpartial_progressiveRendering(app: App) {
//#region App_ontoolinputpartial_progressiveRendering //#region App_ontoolinputpartial_progressiveRendering
let toolInputs: Record<string, unknown> | null = null; const codePreview = document.querySelector<HTMLPreElement>("#code-preview")!;
let toolInputsPartial: Record<string, unknown> | null = null; const canvas = document.querySelector<HTMLCanvasElement>("#canvas")!;
app.ontoolinputpartial = (params) => { app.ontoolinputpartial = (params) => {
toolInputsPartial = params.arguments as Record<string, unknown>; codePreview.textContent = (params.arguments?.code as string) ?? "";
render(); codePreview.style.display = "block";
canvas.style.display = "none";
}; };
app.ontoolinput = (params) => { app.ontoolinput = (params) => {
toolInputs = params.arguments as Record<string, unknown>; codePreview.style.display = "none";
toolInputsPartial = null; canvas.style.display = "block";
render(); render(params.arguments?.code as string);
}; };
function render() {
if (toolInputs) {
renderFinalUI(toolInputs);
} else {
renderLoadingUI(toolInputsPartial); // e.g., shimmer with partial preview
}
}
//#endregion App_ontoolinputpartial_progressiveRendering //#endregion App_ontoolinputpartial_progressiveRendering
} }
// Stubs for App_ontoolinputpartial_progressiveRendering example // Stub for App_ontoolinputpartial_progressiveRendering example
declare function renderLoadingUI(data: Record<string, unknown> | null): void; declare function render(code: string): void;
declare function renderFinalUI(data: Record<string, unknown>): void;
/** /**
* Example: Display tool execution results using ontoolresult. * Example: Display tool execution results using ontoolresult.
@ -205,8 +197,8 @@ function App_ontoolcancelled_handleCancellation(app: App) {
*/ */
function App_onhostcontextchanged_respondToTheme(app: App) { function App_onhostcontextchanged_respondToTheme(app: App) {
//#region App_onhostcontextchanged_respondToTheme //#region App_onhostcontextchanged_respondToTheme
app.onhostcontextchanged = (params) => { app.onhostcontextchanged = (ctx) => {
if (params.theme === "dark") { if (ctx.theme === "dark") {
document.body.classList.add("dark-theme"); document.body.classList.add("dark-theme");
} else { } else {
document.body.classList.remove("dark-theme"); document.body.classList.remove("dark-theme");
@ -220,10 +212,19 @@ function App_onhostcontextchanged_respondToTheme(app: App) {
*/ */
function App_onhostcontextchanged_respondToDisplayMode(app: App) { function App_onhostcontextchanged_respondToDisplayMode(app: App) {
//#region App_onhostcontextchanged_respondToDisplayMode //#region App_onhostcontextchanged_respondToDisplayMode
app.onhostcontextchanged = (params) => { app.onhostcontextchanged = (ctx) => {
if (params.displayMode) { // Adjust to current display mode
const isFullscreen = params.displayMode === "fullscreen"; if (ctx.displayMode) {
document.body.classList.toggle("fullscreen", isFullscreen); const container = document.getElementById("main")!;
const isFullscreen = ctx.displayMode === "fullscreen";
container.classList.toggle("fullscreen", isFullscreen);
}
// Adjust display mode controls
if (ctx.availableDisplayModes) {
const fullscreenBtn = document.getElementById("fullscreen-btn")!;
const canFullscreen = ctx.availableDisplayModes.includes("fullscreen");
fullscreenBtn.style.display = canFullscreen ? "block" : "none";
} }
}; };
//#endregion App_onhostcontextchanged_respondToDisplayMode //#endregion App_onhostcontextchanged_respondToDisplayMode
@ -424,11 +425,12 @@ async function App_openLink_documentation(app: App) {
*/ */
async function App_requestDisplayMode_toggle(app: App) { async function App_requestDisplayMode_toggle(app: App) {
//#region App_requestDisplayMode_toggle //#region App_requestDisplayMode_toggle
const container = document.getElementById("main")!;
const ctx = app.getHostContext(); const ctx = app.getHostContext();
if (ctx?.availableDisplayModes?.includes("fullscreen")) { const newMode = ctx?.displayMode === "inline" ? "fullscreen" : "inline";
const target = ctx.displayMode === "fullscreen" ? "inline" : "fullscreen"; if (ctx?.availableDisplayModes?.includes(newMode)) {
const result = await app.requestDisplayMode({ mode: target }); const result = await app.requestDisplayMode({ mode: newMode });
console.log("Now in:", result.mode); container.classList.toggle("fullscreen", result.mode === "fullscreen");
} }
//#endregion App_requestDisplayMode_toggle //#endregion App_requestDisplayMode_toggle
} }

View file

@ -358,27 +358,20 @@ export class App extends Protocol<AppRequest, AppNotification, AppResult> {
* *
* @example Progressive rendering of tool arguments * @example Progressive rendering of tool arguments
* ```ts source="./app.examples.ts#App_ontoolinputpartial_progressiveRendering" * ```ts source="./app.examples.ts#App_ontoolinputpartial_progressiveRendering"
* let toolInputs: Record<string, unknown> | null = null; * const codePreview = document.querySelector<HTMLPreElement>("#code-preview")!;
* let toolInputsPartial: Record<string, unknown> | null = null; * const canvas = document.querySelector<HTMLCanvasElement>("#canvas")!;
* *
* app.ontoolinputpartial = (params) => { * app.ontoolinputpartial = (params) => {
* toolInputsPartial = params.arguments as Record<string, unknown>; * codePreview.textContent = (params.arguments?.code as string) ?? "";
* render(); * codePreview.style.display = "block";
* canvas.style.display = "none";
* }; * };
* *
* app.ontoolinput = (params) => { * app.ontoolinput = (params) => {
* toolInputs = params.arguments as Record<string, unknown>; * codePreview.style.display = "none";
* toolInputsPartial = null; * canvas.style.display = "block";
* render(); * render(params.arguments?.code as string);
* }; * };
*
* function render() {
* if (toolInputs) {
* renderFinalUI(toolInputs);
* } else {
* renderLoadingUI(toolInputsPartial); // e.g., shimmer with partial preview
* }
* }
* ``` * ```
* *
* @see {@link setNotificationHandler `setNotificationHandler`} for the underlying method * @see {@link setNotificationHandler `setNotificationHandler`} for the underlying method
@ -486,8 +479,8 @@ export class App extends Protocol<AppRequest, AppNotification, AppResult> {
* *
* @example Respond to theme changes * @example Respond to theme changes
* ```ts source="./app.examples.ts#App_onhostcontextchanged_respondToTheme" * ```ts source="./app.examples.ts#App_onhostcontextchanged_respondToTheme"
* app.onhostcontextchanged = (params) => { * app.onhostcontextchanged = (ctx) => {
* if (params.theme === "dark") { * if (ctx.theme === "dark") {
* document.body.classList.add("dark-theme"); * document.body.classList.add("dark-theme");
* } else { * } else {
* document.body.classList.remove("dark-theme"); * document.body.classList.remove("dark-theme");
@ -945,11 +938,12 @@ export class App extends Protocol<AppRequest, AppNotification, AppResult> {
* *
* @example Toggle display mode * @example Toggle display mode
* ```ts source="./app.examples.ts#App_requestDisplayMode_toggle" * ```ts source="./app.examples.ts#App_requestDisplayMode_toggle"
* const container = document.getElementById("main")!;
* const ctx = app.getHostContext(); * const ctx = app.getHostContext();
* if (ctx?.availableDisplayModes?.includes("fullscreen")) { * const newMode = ctx?.displayMode === "inline" ? "fullscreen" : "inline";
* const target = ctx.displayMode === "fullscreen" ? "inline" : "fullscreen"; * if (ctx?.availableDisplayModes?.includes(newMode)) {
* const result = await app.requestDisplayMode({ mode: target }); * const result = await app.requestDisplayMode({ mode: newMode });
* console.log("Now in:", result.mode); * container.classList.toggle("fullscreen", result.mode === "fullscreen");
* } * }
* ``` * ```
* *

View file

@ -51,8 +51,8 @@ function useApp_basicUsage() {
app.onerror = (error) => { app.onerror = (error) => {
console.log("Error:", error); console.log("Error:", error);
}; };
app.onhostcontextchanged = (params) => { app.onhostcontextchanged = (ctx) => {
setHostContext((prev) => ({ ...prev, ...params })); setHostContext((prev) => ({ ...prev, ...ctx }));
}; };
}, },
}); });

View file

@ -102,8 +102,8 @@ export interface AppState {
* app.onerror = (error) => { * app.onerror = (error) => {
* console.log("Error:", error); * console.log("Error:", error);
* }; * };
* app.onhostcontextchanged = (params) => { * app.onhostcontextchanged = (ctx) => {
* setHostContext((prev) => ({ ...prev, ...params })); * setHostContext((prev) => ({ ...prev, ...ctx }));
* }; * };
* }, * },
* }); * });

View file

@ -31,9 +31,9 @@ function getDocumentTheme_checkCurrent() {
function applyDocumentTheme_fromHostContext(app: App) { function applyDocumentTheme_fromHostContext(app: App) {
//#region applyDocumentTheme_fromHostContext //#region applyDocumentTheme_fromHostContext
// Apply when host context changes // Apply when host context changes
app.onhostcontextchanged = (params) => { app.onhostcontextchanged = (ctx) => {
if (params.theme) { if (ctx.theme) {
applyDocumentTheme(params.theme); applyDocumentTheme(ctx.theme);
} }
}; };
@ -56,9 +56,9 @@ function applyHostStyleVariables_fromHostContext(app: App) {
document.body.style.background = "var(--color-background-primary)"; document.body.style.background = "var(--color-background-primary)";
// Apply when host context changes // Apply when host context changes
app.onhostcontextchanged = (params) => { app.onhostcontextchanged = (ctx) => {
if (params.styles?.variables) { if (ctx.styles?.variables) {
applyHostStyleVariables(params.styles.variables); applyHostStyleVariables(ctx.styles.variables);
} }
}; };
@ -77,10 +77,10 @@ function applyHostStyleVariables_fromHostContext(app: App) {
*/ */
function applyHostStyleVariables_toElement(app: App) { function applyHostStyleVariables_toElement(app: App) {
//#region applyHostStyleVariables_toElement //#region applyHostStyleVariables_toElement
app.onhostcontextchanged = (params) => { app.onhostcontextchanged = (ctx) => {
const container = document.getElementById("app-root"); const container = document.getElementById("app-root");
if (container && params.styles?.variables) { if (container && ctx.styles?.variables) {
applyHostStyleVariables(params.styles.variables, container); applyHostStyleVariables(ctx.styles.variables, container);
} }
}; };
//#endregion applyHostStyleVariables_toElement //#endregion applyHostStyleVariables_toElement
@ -92,9 +92,9 @@ function applyHostStyleVariables_toElement(app: App) {
function applyHostFonts_fromHostContext(app: App) { function applyHostFonts_fromHostContext(app: App) {
//#region applyHostFonts_fromHostContext //#region applyHostFonts_fromHostContext
// Apply when host context changes // Apply when host context changes
app.onhostcontextchanged = (params) => { app.onhostcontextchanged = (ctx) => {
if (params.styles?.css?.fonts) { if (ctx.styles?.css?.fonts) {
applyHostFonts(params.styles.css.fonts); applyHostFonts(ctx.styles.css.fonts);
} }
}; };

View file

@ -44,9 +44,9 @@ export function getDocumentTheme(): McpUiTheme {
* @example Apply theme from host context * @example Apply theme from host context
* ```ts source="./styles.examples.ts#applyDocumentTheme_fromHostContext" * ```ts source="./styles.examples.ts#applyDocumentTheme_fromHostContext"
* // Apply when host context changes * // Apply when host context changes
* app.onhostcontextchanged = (params) => { * app.onhostcontextchanged = (ctx) => {
* if (params.theme) { * if (ctx.theme) {
* applyDocumentTheme(params.theme); * applyDocumentTheme(ctx.theme);
* } * }
* }; * };
* *
@ -95,9 +95,9 @@ export function applyDocumentTheme(theme: McpUiTheme): void {
* document.body.style.background = "var(--color-background-primary)"; * document.body.style.background = "var(--color-background-primary)";
* *
* // Apply when host context changes * // Apply when host context changes
* app.onhostcontextchanged = (params) => { * app.onhostcontextchanged = (ctx) => {
* if (params.styles?.variables) { * if (ctx.styles?.variables) {
* applyHostStyleVariables(params.styles.variables); * applyHostStyleVariables(ctx.styles.variables);
* } * }
* }; * };
* *
@ -112,10 +112,10 @@ export function applyDocumentTheme(theme: McpUiTheme): void {
* *
* @example Apply to a specific element * @example Apply to a specific element
* ```ts source="./styles.examples.ts#applyHostStyleVariables_toElement" * ```ts source="./styles.examples.ts#applyHostStyleVariables_toElement"
* app.onhostcontextchanged = (params) => { * app.onhostcontextchanged = (ctx) => {
* const container = document.getElementById("app-root"); * const container = document.getElementById("app-root");
* if (container && params.styles?.variables) { * if (container && ctx.styles?.variables) {
* applyHostStyleVariables(params.styles.variables, container); * applyHostStyleVariables(ctx.styles.variables, container);
* } * }
* }; * };
* ``` * ```
@ -163,9 +163,9 @@ export function applyHostStyleVariables(
* @example Apply fonts from host context * @example Apply fonts from host context
* ```ts source="./styles.examples.ts#applyHostFonts_fromHostContext" * ```ts source="./styles.examples.ts#applyHostFonts_fromHostContext"
* // Apply when host context changes * // Apply when host context changes
* app.onhostcontextchanged = (params) => { * app.onhostcontextchanged = (ctx) => {
* if (params.styles?.css?.fonts) { * if (ctx.styles?.css?.fonts) {
* applyHostFonts(params.styles.css.fonts); * applyHostFonts(ctx.styles.css.fonts);
* } * }
* }; * };
* *