Updating viewport type and documentation

This commit is contained in:
martinalong 2025-12-16 21:52:39 -08:00
commit 93b9da9e7a
No known key found for this signature in database
6 changed files with 342 additions and 180 deletions

View file

@ -461,13 +461,18 @@ interface HostContext {
displayMode?: "inline" | "fullscreen" | "pip"; displayMode?: "inline" | "fullscreen" | "pip";
/** Display modes the host supports */ /** Display modes the host supports */
availableDisplayModes?: string[]; availableDisplayModes?: string[];
/** Current and maximum dimensions available to the UI */ /**
viewport?: { * Viewport dimensions available to the UI.
width: number; *
height: number; * The viewport has two independent dimension pairs:
maxHeight?: number; * - Height: Either `height` (fixed) or `maxHeight` (flexible), never both
maxWidth?: number; * - Width: Either `width` (fixed) or `maxWidth` (flexible), never both
}; *
* Fixed dimensions (height/width): The host controls the size. Set height: 100% (recommended) or use the pixel value directly.
* Flexible dimensions (maxHeight/maxWidth or undefined): The app controls the size, up to the max if specified.
*/
viewport?: ({ height: number } | { maxHeight?: number }) &
({ width: number } | { maxWidth?: number });
/** User's language/region preference (BCP 47, e.g., "en-US") */ /** User's language/region preference (BCP 47, e.g., "en-US") */
locale?: string; locale?: string;
/** User's timezone (IANA, e.g., "America/New_York") */ /** User's timezone (IANA, e.g., "America/New_York") */
@ -516,12 +521,76 @@ Example:
} }
}, },
"displayMode": "inline", "displayMode": "inline",
"viewport": { "width": 400, "height": 300 } "viewport": { "width": 400, "maxHeight": 600 }
} }
} }
} }
``` ```
### Viewport and Sizing
The `viewport` field in `HostContext` communicates sizing constraints between host and app. Each dimension (height and width) operates independently and can be either **fixed** or **flexible**.
#### Viewport Modes
| Mode | Viewport Field | Meaning |
|------|---------------|---------|
| Fixed | `height` or `width` | Host controls the size. App should fill the available space. |
| Flexible | `maxHeight` or `maxWidth` | App controls the size, up to the specified maximum. |
| Unbounded | Field omitted | App controls the size with no limit. |
These modes can be combined independently. For example, a host might specify a fixed width but flexible height, allowing the app to grow vertically based on content.
#### App Behavior
Apps should check the viewport configuration and apply appropriate CSS:
```typescript
// In the app's initialization
const viewport = hostContext.viewport;
if (viewport) {
// Handle height
if ("height" in viewport) {
// Fixed height: fill the container
document.body.style.height = "100%";
} else if (viewport.maxHeight) {
// Flexible with max: let content determine size, up to max
document.body.style.maxHeight = `${viewport.maxHeight}px`;
}
// If neither, height is unbounded
// Handle width
if ("width" in viewport) {
// Fixed width: fill the container
document.body.style.width = "100%";
} else if (viewport.maxWidth) {
// Flexible with max: let content determine size, up to max
document.body.style.maxWidth = `${viewport.maxWidth}px`;
}
// If neither, width is unbounded
}
```
#### Host Behavior
When using flexible dimensions (no fixed `height` or `width`), hosts MUST listen for `ui/notifications/size-changed` notifications from the app and update the iframe dimensions accordingly:
```typescript
// Host listens for size changes from the app
bridge.onsizechange = ({ width, height }) => {
// Update iframe to match app's content size
if (width != null) {
iframe.style.width = `${width}px`;
}
if (height != null) {
iframe.style.height = `${height}px`;
}
};
```
Apps using the SDK automatically send size-changed notifications via ResizeObserver when `autoResize` is enabled (the default). The notifications are debounced and only sent when dimensions actually change.
### Theming ### Theming
Hosts can optionally pass CSS custom properties via `HostContext.styles.variables` for visual cohesion with the host environment. Hosts can optionally pass CSS custom properties via `HostContext.styles.variables` for visual cohesion with the host environment.

View file

@ -113,7 +113,7 @@ describe("App <-> AppBridge integration", () => {
const testHostContext = { const testHostContext = {
theme: "dark" as const, theme: "dark" as const,
locale: "en-US", locale: "en-US",
viewport: { width: 800, height: 600 }, viewport: { width: 800, maxHeight: 600 },
}; };
const newBridge = new AppBridge( const newBridge = new AppBridge(
createMockClient() as Client, createMockClient() as Client,
@ -337,7 +337,7 @@ describe("App <-> AppBridge integration", () => {
const initialContext = { const initialContext = {
theme: "light" as const, theme: "light" as const,
locale: "en-US", locale: "en-US",
viewport: { width: 800, height: 600 }, viewport: { width: 800, maxHeight: 600 },
}; };
const newBridge = new AppBridge( const newBridge = new AppBridge(
createMockClient() as Client, createMockClient() as Client,
@ -356,7 +356,7 @@ describe("App <-> AppBridge integration", () => {
// Send another partial update: only viewport changes // Send another partial update: only viewport changes
newBridge.sendHostContextChange({ newBridge.sendHostContextChange({
viewport: { width: 1024, height: 768 }, viewport: { width: 1024, maxHeight: 768 },
}); });
await flush(); await flush();
@ -367,7 +367,7 @@ describe("App <-> AppBridge integration", () => {
const context = newApp.getHostContext(); const context = newApp.getHostContext();
expect(context?.theme).toBe("dark"); expect(context?.theme).toBe("dark");
expect(context?.locale).toBe("en-US"); expect(context?.locale).toBe("en-US");
expect(context?.viewport).toEqual({ width: 1024, height: 768 }); expect(context?.viewport).toEqual({ width: 1024, maxHeight: 768 });
await newAppTransport.close(); await newAppTransport.close();
await newBridgeTransport.close(); await newBridgeTransport.close();

View file

@ -1008,7 +1008,7 @@ export class AppBridge extends Protocol<
* ```typescript * ```typescript
* bridge.setHostContext({ * bridge.setHostContext({
* theme: "dark", * theme: "dark",
* viewport: { width: 800, height: 600 } * viewport: { width: 800, maxHeight: 600 }
* }); * });
* ``` * ```
* *

View file

@ -629,28 +629,55 @@
} }
}, },
"viewport": { "viewport": {
"description": "Current and maximum dimensions available to the UI.", "description": "Viewport dimensions available to the UI.\n\nThe viewport has two independent dimension pairs:\n- Height: Either `height` (fixed) or `maxHeight` (flexible), never both\n- Width: Either `width` (fixed) or `maxWidth` (flexible), never both\n\nFixed dimensions (height/width): The host controls the size. Set height: 100% (recommended) or use the pixel value directly.\nFlexible dimensions (maxHeight/maxWidth or undefined): The app controls the size, up to the max if specified. If undefined, there is no limit.",
"type": "object", "allOf": [
"properties": { {
"width": { "anyOf": [
"description": "Current viewport width in pixels.", {
"type": "number" "type": "object",
"properties": {
"height": {
"type": "number"
}
},
"required": ["height"],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"maxHeight": {
"type": "number"
}
},
"additionalProperties": false
}
]
}, },
"height": { {
"description": "Current viewport height in pixels.", "anyOf": [
"type": "number" {
}, "type": "object",
"maxHeight": { "properties": {
"description": "Maximum available height in pixels (if constrained).", "width": {
"type": "number" "type": "number"
}, }
"maxWidth": { },
"description": "Maximum available width in pixels (if constrained).", "required": ["width"],
"type": "number" "additionalProperties": false
},
{
"type": "object",
"properties": {
"maxWidth": {
"type": "number"
}
},
"additionalProperties": false
}
]
} }
}, ]
"required": ["width", "height"],
"additionalProperties": false
}, },
"locale": { "locale": {
"description": "User's language and region preference in BCP 47 format.", "description": "User's language and region preference in BCP 47 format.",
@ -1255,28 +1282,55 @@
} }
}, },
"viewport": { "viewport": {
"description": "Current and maximum dimensions available to the UI.", "description": "Viewport dimensions available to the UI.\n\nThe viewport has two independent dimension pairs:\n- Height: Either `height` (fixed) or `maxHeight` (flexible), never both\n- Width: Either `width` (fixed) or `maxWidth` (flexible), never both\n\nFixed dimensions (height/width): The host controls the size. Set height: 100% (recommended) or use the pixel value directly.\nFlexible dimensions (maxHeight/maxWidth or undefined): The app controls the size, up to the max if specified. If undefined, there is no limit.",
"type": "object", "allOf": [
"properties": { {
"width": { "anyOf": [
"description": "Current viewport width in pixels.", {
"type": "number" "type": "object",
"properties": {
"height": {
"type": "number"
}
},
"required": ["height"],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"maxHeight": {
"type": "number"
}
},
"additionalProperties": false
}
]
}, },
"height": { {
"description": "Current viewport height in pixels.", "anyOf": [
"type": "number" {
}, "type": "object",
"maxHeight": { "properties": {
"description": "Maximum available height in pixels (if constrained).", "width": {
"type": "number" "type": "number"
}, }
"maxWidth": { },
"description": "Maximum available width in pixels (if constrained).", "required": ["width"],
"type": "number" "additionalProperties": false
},
{
"type": "object",
"properties": {
"maxWidth": {
"type": "number"
}
},
"additionalProperties": false
}
]
} }
}, ]
"required": ["width", "height"],
"additionalProperties": false
}, },
"locale": { "locale": {
"description": "User's language and region preference in BCP 47 format.", "description": "User's language and region preference in BCP 47 format.",
@ -2414,28 +2468,55 @@
} }
}, },
"viewport": { "viewport": {
"description": "Current and maximum dimensions available to the UI.", "description": "Viewport dimensions available to the UI.\n\nThe viewport has two independent dimension pairs:\n- Height: Either `height` (fixed) or `maxHeight` (flexible), never both\n- Width: Either `width` (fixed) or `maxWidth` (flexible), never both\n\nFixed dimensions (height/width): The host controls the size. Set height: 100% (recommended) or use the pixel value directly.\nFlexible dimensions (maxHeight/maxWidth or undefined): The app controls the size, up to the max if specified. If undefined, there is no limit.",
"type": "object", "allOf": [
"properties": { {
"width": { "anyOf": [
"description": "Current viewport width in pixels.", {
"type": "number" "type": "object",
"properties": {
"height": {
"type": "number"
}
},
"required": ["height"],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"maxHeight": {
"type": "number"
}
},
"additionalProperties": false
}
]
}, },
"height": { {
"description": "Current viewport height in pixels.", "anyOf": [
"type": "number" {
}, "type": "object",
"maxHeight": { "properties": {
"description": "Maximum available height in pixels (if constrained).", "width": {
"type": "number" "type": "number"
}, }
"maxWidth": { },
"description": "Maximum available width in pixels (if constrained).", "required": ["width"],
"type": "number" "additionalProperties": false
},
{
"type": "object",
"properties": {
"maxWidth": {
"type": "number"
}
},
"additionalProperties": false
}
]
} }
}, ]
"required": ["width", "height"],
"additionalProperties": false
}, },
"locale": { "locale": {
"description": "User's language and region preference in BCP 47 format.", "description": "User's language and region preference in BCP 47 format.",

203
src/generated/schema.ts generated
View file

@ -532,103 +532,114 @@ export const McpUiToolResultNotificationSchema = z.object({
/** /**
* @description Rich context about the host environment provided to Guest UIs. * @description Rich context about the host environment provided to Guest UIs.
*/ */
export const McpUiHostContextSchema = z export const McpUiHostContextSchema = z.looseObject({
.object({ /** @description Metadata of the tool call that instantiated this App. */
/** @description Metadata of the tool call that instantiated this App. */ toolInfo: z
toolInfo: z .object({
.object({ /** @description JSON-RPC id of the tools/call request. */
/** @description JSON-RPC id of the tools/call request. */ id: RequestIdSchema.describe("JSON-RPC id of the tools/call request."),
id: RequestIdSchema.describe("JSON-RPC id of the tools/call request."), /** @description Tool definition including name, inputSchema, etc. */
/** @description Tool definition including name, inputSchema, etc. */ tool: ToolSchema.describe(
tool: ToolSchema.describe( "Tool definition including name, inputSchema, etc.",
"Tool definition including name, inputSchema, etc.", ),
), })
}) .optional()
.optional() .describe("Metadata of the tool call that instantiated this App."),
.describe("Metadata of the tool call that instantiated this App."), /** @description Current color theme preference. */
/** @description Current color theme preference. */ theme: McpUiThemeSchema.optional().describe(
theme: McpUiThemeSchema.optional().describe( "Current color theme preference.",
"Current color theme preference.", ),
/** @description Style configuration for theming the app. */
styles: McpUiHostStylesSchema.optional().describe(
"Style configuration for theming the app.",
),
/** @description How the UI is currently displayed. */
displayMode: McpUiDisplayModeSchema.optional().describe(
"How the UI is currently displayed.",
),
/** @description Display modes the host supports. */
availableDisplayModes: z
.array(z.string())
.optional()
.describe("Display modes the host supports."),
/**
* @description Viewport dimensions available to the UI.
*
* The viewport has two independent dimension pairs:
* - Height: Either `height` (fixed) or `maxHeight` (flexible), never both
* - Width: Either `width` (fixed) or `maxWidth` (flexible), never both
*
* Fixed dimensions (height/width): The host controls the size. Set height: 100% (recommended) or use the pixel value directly.
* Flexible dimensions (maxHeight/maxWidth or undefined): The app controls the size, up to the max if specified. If undefined, there is no limit.
*/
viewport: z
.union([
z.object({
height: z.number(),
}),
z.object({
maxHeight: z.number().optional(),
}),
])
.and(
z.union([
z.object({
width: z.number(),
}),
z.object({
maxWidth: z.number().optional(),
}),
]),
)
.optional()
.describe(
"Viewport dimensions available to the UI.\n\nThe viewport has two independent dimension pairs:\n- Height: Either `height` (fixed) or `maxHeight` (flexible), never both\n- Width: Either `width` (fixed) or `maxWidth` (flexible), never both\n\nFixed dimensions (height/width): The host controls the size. Set height: 100% (recommended) or use the pixel value directly.\nFlexible dimensions (maxHeight/maxWidth or undefined): The app controls the size, up to the max if specified. If undefined, there is no limit.",
), ),
/** @description Style configuration for theming the app. */ /** @description User's language and region preference in BCP 47 format. */
styles: McpUiHostStylesSchema.optional().describe( locale: z
"Style configuration for theming the app.", .string()
), .optional()
/** @description How the UI is currently displayed. */ .describe("User's language and region preference in BCP 47 format."),
displayMode: McpUiDisplayModeSchema.optional().describe( /** @description User's timezone in IANA format. */
"How the UI is currently displayed.", timeZone: z.string().optional().describe("User's timezone in IANA format."),
), /** @description Host application identifier. */
/** @description Display modes the host supports. */ userAgent: z.string().optional().describe("Host application identifier."),
availableDisplayModes: z /** @description Platform type for responsive design decisions. */
.array(z.string()) platform: z
.optional() .union([z.literal("web"), z.literal("desktop"), z.literal("mobile")])
.describe("Display modes the host supports."), .optional()
/** @description Current and maximum dimensions available to the UI. */ .describe("Platform type for responsive design decisions."),
viewport: z /** @description Device input capabilities. */
.object({ deviceCapabilities: z
/** @description Current viewport width in pixels. */ .object({
width: z.number().describe("Current viewport width in pixels."), /** @description Whether the device supports touch input. */
/** @description Current viewport height in pixels. */ touch: z
height: z.number().describe("Current viewport height in pixels."), .boolean()
/** @description Maximum available height in pixels (if constrained). */ .optional()
maxHeight: z .describe("Whether the device supports touch input."),
.number() /** @description Whether the device supports hover interactions. */
.optional() hover: z
.describe("Maximum available height in pixels (if constrained)."), .boolean()
/** @description Maximum available width in pixels (if constrained). */ .optional()
maxWidth: z .describe("Whether the device supports hover interactions."),
.number() })
.optional() .optional()
.describe("Maximum available width in pixels (if constrained)."), .describe("Device input capabilities."),
}) /** @description Mobile safe area boundaries in pixels. */
.optional() safeAreaInsets: z
.describe("Current and maximum dimensions available to the UI."), .object({
/** @description User's language and region preference in BCP 47 format. */ /** @description Top safe area inset in pixels. */
locale: z top: z.number().describe("Top safe area inset in pixels."),
.string() /** @description Right safe area inset in pixels. */
.optional() right: z.number().describe("Right safe area inset in pixels."),
.describe("User's language and region preference in BCP 47 format."), /** @description Bottom safe area inset in pixels. */
/** @description User's timezone in IANA format. */ bottom: z.number().describe("Bottom safe area inset in pixels."),
timeZone: z.string().optional().describe("User's timezone in IANA format."), /** @description Left safe area inset in pixels. */
/** @description Host application identifier. */ left: z.number().describe("Left safe area inset in pixels."),
userAgent: z.string().optional().describe("Host application identifier."), })
/** @description Platform type for responsive design decisions. */ .optional()
platform: z .describe("Mobile safe area boundaries in pixels."),
.union([z.literal("web"), z.literal("desktop"), z.literal("mobile")]) });
.optional()
.describe("Platform type for responsive design decisions."),
/** @description Device input capabilities. */
deviceCapabilities: z
.object({
/** @description Whether the device supports touch input. */
touch: z
.boolean()
.optional()
.describe("Whether the device supports touch input."),
/** @description Whether the device supports hover interactions. */
hover: z
.boolean()
.optional()
.describe("Whether the device supports hover interactions."),
})
.optional()
.describe("Device input capabilities."),
/** @description Mobile safe area boundaries in pixels. */
safeAreaInsets: z
.object({
/** @description Top safe area inset in pixels. */
top: z.number().describe("Top safe area inset in pixels."),
/** @description Right safe area inset in pixels. */
right: z.number().describe("Right safe area inset in pixels."),
/** @description Bottom safe area inset in pixels. */
bottom: z.number().describe("Bottom safe area inset in pixels."),
/** @description Left safe area inset in pixels. */
left: z.number().describe("Left safe area inset in pixels."),
})
.optional()
.describe("Mobile safe area boundaries in pixels."),
})
.passthrough();
/** /**
* @description Notification that host context has changed (Host -> Guest UI). * @description Notification that host context has changed (Host -> Guest UI).

View file

@ -324,17 +324,18 @@ export interface McpUiHostContext {
displayMode?: McpUiDisplayMode; displayMode?: McpUiDisplayMode;
/** @description Display modes the host supports. */ /** @description Display modes the host supports. */
availableDisplayModes?: string[]; availableDisplayModes?: string[];
/** @description Current and maximum dimensions available to the UI. */ /**
viewport?: { * @description Viewport dimensions available to the UI.
/** @description Current viewport width in pixels. */ *
width: number; * The viewport has two independent dimension pairs:
/** @description Current viewport height in pixels. */ * - Height: Either `height` (fixed) or `maxHeight` (flexible), never both
height: number; * - Width: Either `width` (fixed) or `maxWidth` (flexible), never both
/** @description Maximum available height in pixels (if constrained). */ *
maxHeight?: number; * Fixed dimensions (height/width): The host controls the size. Set height: 100% (recommended) or use the pixel value directly.
/** @description Maximum available width in pixels (if constrained). */ * Flexible dimensions (maxHeight/maxWidth or undefined): The app controls the size, up to the max if specified. If undefined, there is no limit.
maxWidth?: number; */
}; viewport?: ({ height: number } | { maxHeight?: number }) &
({ width: number } | { maxWidth?: number });
/** @description User's language and region preference in BCP 47 format. */ /** @description User's language and region preference in BCP 47 format. */
locale?: string; locale?: string;
/** @description User's timezone in IANA format. */ /** @description User's timezone in IANA format. */