From 93b9da9e7a476c40f478db78019b8c20899082f5 Mon Sep 17 00:00:00 2001 From: martinalong Date: Tue, 16 Dec 2025 21:52:39 -0800 Subject: [PATCH 1/8] Updating viewport type and documentation --- specification/draft/apps.mdx | 85 +++++++++++++-- src/app-bridge.test.ts | 8 +- src/app-bridge.ts | 2 +- src/generated/schema.json | 201 +++++++++++++++++++++++----------- src/generated/schema.ts | 203 ++++++++++++++++++----------------- src/spec.types.ts | 23 ++-- 6 files changed, 342 insertions(+), 180 deletions(-) diff --git a/specification/draft/apps.mdx b/specification/draft/apps.mdx index 6e33f81..e5ff305 100644 --- a/specification/draft/apps.mdx +++ b/specification/draft/apps.mdx @@ -461,13 +461,18 @@ interface HostContext { displayMode?: "inline" | "fullscreen" | "pip"; /** Display modes the host supports */ availableDisplayModes?: string[]; - /** Current and maximum dimensions available to the UI */ - viewport?: { - width: number; - height: number; - maxHeight?: number; - maxWidth?: number; - }; + /** + * 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. + */ + viewport?: ({ height: number } | { maxHeight?: number }) & + ({ width: number } | { maxWidth?: number }); /** User's language/region preference (BCP 47, e.g., "en-US") */ locale?: string; /** User's timezone (IANA, e.g., "America/New_York") */ @@ -516,12 +521,76 @@ Example: } }, "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 Hosts can optionally pass CSS custom properties via `HostContext.styles.variables` for visual cohesion with the host environment. diff --git a/src/app-bridge.test.ts b/src/app-bridge.test.ts index 969030f..3424d69 100644 --- a/src/app-bridge.test.ts +++ b/src/app-bridge.test.ts @@ -113,7 +113,7 @@ describe("App <-> AppBridge integration", () => { const testHostContext = { theme: "dark" as const, locale: "en-US", - viewport: { width: 800, height: 600 }, + viewport: { width: 800, maxHeight: 600 }, }; const newBridge = new AppBridge( createMockClient() as Client, @@ -337,7 +337,7 @@ describe("App <-> AppBridge integration", () => { const initialContext = { theme: "light" as const, locale: "en-US", - viewport: { width: 800, height: 600 }, + viewport: { width: 800, maxHeight: 600 }, }; const newBridge = new AppBridge( createMockClient() as Client, @@ -356,7 +356,7 @@ describe("App <-> AppBridge integration", () => { // Send another partial update: only viewport changes newBridge.sendHostContextChange({ - viewport: { width: 1024, height: 768 }, + viewport: { width: 1024, maxHeight: 768 }, }); await flush(); @@ -367,7 +367,7 @@ describe("App <-> AppBridge integration", () => { const context = newApp.getHostContext(); expect(context?.theme).toBe("dark"); 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 newBridgeTransport.close(); diff --git a/src/app-bridge.ts b/src/app-bridge.ts index 2c63eaa..111be34 100644 --- a/src/app-bridge.ts +++ b/src/app-bridge.ts @@ -1008,7 +1008,7 @@ export class AppBridge extends Protocol< * ```typescript * bridge.setHostContext({ * theme: "dark", - * viewport: { width: 800, height: 600 } + * viewport: { width: 800, maxHeight: 600 } * }); * ``` * diff --git a/src/generated/schema.json b/src/generated/schema.json index e835986..f1a458c 100644 --- a/src/generated/schema.json +++ b/src/generated/schema.json @@ -629,28 +629,55 @@ } }, "viewport": { - "description": "Current and maximum dimensions available to the UI.", - "type": "object", - "properties": { - "width": { - "description": "Current viewport width in pixels.", - "type": "number" + "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.", + "allOf": [ + { + "anyOf": [ + { + "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.", - "type": "number" - }, - "maxHeight": { - "description": "Maximum available height in pixels (if constrained).", - "type": "number" - }, - "maxWidth": { - "description": "Maximum available width in pixels (if constrained).", - "type": "number" + { + "anyOf": [ + { + "type": "object", + "properties": { + "width": { + "type": "number" + } + }, + "required": ["width"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "maxWidth": { + "type": "number" + } + }, + "additionalProperties": false + } + ] } - }, - "required": ["width", "height"], - "additionalProperties": false + ] }, "locale": { "description": "User's language and region preference in BCP 47 format.", @@ -1255,28 +1282,55 @@ } }, "viewport": { - "description": "Current and maximum dimensions available to the UI.", - "type": "object", - "properties": { - "width": { - "description": "Current viewport width in pixels.", - "type": "number" + "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.", + "allOf": [ + { + "anyOf": [ + { + "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.", - "type": "number" - }, - "maxHeight": { - "description": "Maximum available height in pixels (if constrained).", - "type": "number" - }, - "maxWidth": { - "description": "Maximum available width in pixels (if constrained).", - "type": "number" + { + "anyOf": [ + { + "type": "object", + "properties": { + "width": { + "type": "number" + } + }, + "required": ["width"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "maxWidth": { + "type": "number" + } + }, + "additionalProperties": false + } + ] } - }, - "required": ["width", "height"], - "additionalProperties": false + ] }, "locale": { "description": "User's language and region preference in BCP 47 format.", @@ -2414,28 +2468,55 @@ } }, "viewport": { - "description": "Current and maximum dimensions available to the UI.", - "type": "object", - "properties": { - "width": { - "description": "Current viewport width in pixels.", - "type": "number" + "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.", + "allOf": [ + { + "anyOf": [ + { + "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.", - "type": "number" - }, - "maxHeight": { - "description": "Maximum available height in pixels (if constrained).", - "type": "number" - }, - "maxWidth": { - "description": "Maximum available width in pixels (if constrained).", - "type": "number" + { + "anyOf": [ + { + "type": "object", + "properties": { + "width": { + "type": "number" + } + }, + "required": ["width"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "maxWidth": { + "type": "number" + } + }, + "additionalProperties": false + } + ] } - }, - "required": ["width", "height"], - "additionalProperties": false + ] }, "locale": { "description": "User's language and region preference in BCP 47 format.", diff --git a/src/generated/schema.ts b/src/generated/schema.ts index 9e6120a..5558961 100644 --- a/src/generated/schema.ts +++ b/src/generated/schema.ts @@ -532,103 +532,114 @@ export const McpUiToolResultNotificationSchema = z.object({ /** * @description Rich context about the host environment provided to Guest UIs. */ -export const McpUiHostContextSchema = z - .object({ - /** @description Metadata of the tool call that instantiated this App. */ - toolInfo: z - .object({ - /** @description 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. */ - tool: ToolSchema.describe( - "Tool definition including name, inputSchema, etc.", - ), - }) - .optional() - .describe("Metadata of the tool call that instantiated this App."), - /** @description Current color theme preference. */ - theme: McpUiThemeSchema.optional().describe( - "Current color theme preference.", +export const McpUiHostContextSchema = z.looseObject({ + /** @description Metadata of the tool call that instantiated this App. */ + toolInfo: z + .object({ + /** @description 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. */ + tool: ToolSchema.describe( + "Tool definition including name, inputSchema, etc.", + ), + }) + .optional() + .describe("Metadata of the tool call that instantiated this App."), + /** @description Current color theme preference. */ + theme: McpUiThemeSchema.optional().describe( + "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. */ - 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 Current and maximum dimensions available to the UI. */ - viewport: z - .object({ - /** @description Current viewport width in pixels. */ - width: z.number().describe("Current viewport width in pixels."), - /** @description Current viewport height in pixels. */ - height: z.number().describe("Current viewport height in pixels."), - /** @description Maximum available height in pixels (if constrained). */ - maxHeight: z - .number() - .optional() - .describe("Maximum available height in pixels (if constrained)."), - /** @description Maximum available width in pixels (if constrained). */ - maxWidth: z - .number() - .optional() - .describe("Maximum available width in pixels (if constrained)."), - }) - .optional() - .describe("Current and maximum dimensions available to the UI."), - /** @description User's language and region preference in BCP 47 format. */ - locale: z - .string() - .optional() - .describe("User's language and region preference in BCP 47 format."), - /** @description User's timezone in IANA format. */ - timeZone: z.string().optional().describe("User's timezone in IANA format."), - /** @description Host application identifier. */ - userAgent: z.string().optional().describe("Host application identifier."), - /** @description Platform type for responsive design decisions. */ - platform: z - .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 User's language and region preference in BCP 47 format. */ + locale: z + .string() + .optional() + .describe("User's language and region preference in BCP 47 format."), + /** @description User's timezone in IANA format. */ + timeZone: z.string().optional().describe("User's timezone in IANA format."), + /** @description Host application identifier. */ + userAgent: z.string().optional().describe("Host application identifier."), + /** @description Platform type for responsive design decisions. */ + platform: z + .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."), +}); /** * @description Notification that host context has changed (Host -> Guest UI). diff --git a/src/spec.types.ts b/src/spec.types.ts index f97a0a6..1a0c8c2 100644 --- a/src/spec.types.ts +++ b/src/spec.types.ts @@ -324,17 +324,18 @@ export interface McpUiHostContext { displayMode?: McpUiDisplayMode; /** @description Display modes the host supports. */ availableDisplayModes?: string[]; - /** @description Current and maximum dimensions available to the UI. */ - viewport?: { - /** @description Current viewport width in pixels. */ - width: number; - /** @description Current viewport height in pixels. */ - height: number; - /** @description Maximum available height in pixels (if constrained). */ - maxHeight?: number; - /** @description Maximum available width in pixels (if constrained). */ - maxWidth?: number; - }; + /** + * @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?: ({ height: number } | { maxHeight?: number }) & + ({ width: number } | { maxWidth?: number }); /** @description User's language and region preference in BCP 47 format. */ locale?: string; /** @description User's timezone in IANA format. */ From 87f6f3f27e36645d6287327d586c306c5807abbb Mon Sep 17 00:00:00 2001 From: martinalong Date: Wed, 17 Dec 2025 21:20:14 -0800 Subject: [PATCH 2/8] Update documentation and revert type change --- specification/draft/apps.mdx | 23 ++-- src/app-bridge.test.ts | 12 ++- src/generated/schema.json | 201 +++++++++++------------------------ src/generated/schema.ts | 49 ++++----- src/spec.types.ts | 23 ++-- 5 files changed, 108 insertions(+), 200 deletions(-) diff --git a/specification/draft/apps.mdx b/specification/draft/apps.mdx index e5ff305..f4aa9d5 100644 --- a/specification/draft/apps.mdx +++ b/specification/draft/apps.mdx @@ -461,18 +461,17 @@ interface HostContext { displayMode?: "inline" | "fullscreen" | "pip"; /** Display modes the host supports */ availableDisplayModes?: string[]; - /** - * 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. - */ - viewport?: ({ height: number } | { maxHeight?: number }) & - ({ width: number } | { maxWidth?: number }); + /** Current and maximum dimensions available to the UI. */ + viewport?: { + /** Current viewport width in pixels. */ + width: number; + /** Current viewport height in pixels. */ + height: number; + /** Maximum available height in pixels (if constrained). */ + maxHeight?: number; + /** Maximum available width in pixels (if constrained). */ + maxWidth?: number; + }; /** User's language/region preference (BCP 47, e.g., "en-US") */ locale?: string; /** User's timezone (IANA, e.g., "America/New_York") */ diff --git a/src/app-bridge.test.ts b/src/app-bridge.test.ts index 3424d69..5a36b47 100644 --- a/src/app-bridge.test.ts +++ b/src/app-bridge.test.ts @@ -113,7 +113,7 @@ describe("App <-> AppBridge integration", () => { const testHostContext = { theme: "dark" as const, locale: "en-US", - viewport: { width: 800, maxHeight: 600 }, + viewport: { width: 800, height: 600, maxHeight: 600 }, }; const newBridge = new AppBridge( createMockClient() as Client, @@ -337,7 +337,7 @@ describe("App <-> AppBridge integration", () => { const initialContext = { theme: "light" as const, locale: "en-US", - viewport: { width: 800, maxHeight: 600 }, + viewport: { width: 800, height: 600, maxHeight: 600 }, }; const newBridge = new AppBridge( createMockClient() as Client, @@ -356,7 +356,7 @@ describe("App <-> AppBridge integration", () => { // Send another partial update: only viewport changes newBridge.sendHostContextChange({ - viewport: { width: 1024, maxHeight: 768 }, + viewport: { width: 1024, height: 768, maxHeight: 768 }, }); await flush(); @@ -367,7 +367,11 @@ describe("App <-> AppBridge integration", () => { const context = newApp.getHostContext(); expect(context?.theme).toBe("dark"); expect(context?.locale).toBe("en-US"); - expect(context?.viewport).toEqual({ width: 1024, maxHeight: 768 }); + expect(context?.viewport).toEqual({ + width: 1024, + height: 768, + maxHeight: 768, + }); await newAppTransport.close(); await newBridgeTransport.close(); diff --git a/src/generated/schema.json b/src/generated/schema.json index f1a458c..e835986 100644 --- a/src/generated/schema.json +++ b/src/generated/schema.json @@ -629,55 +629,28 @@ } }, "viewport": { - "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.", - "allOf": [ - { - "anyOf": [ - { - "type": "object", - "properties": { - "height": { - "type": "number" - } - }, - "required": ["height"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "maxHeight": { - "type": "number" - } - }, - "additionalProperties": false - } - ] + "description": "Current and maximum dimensions available to the UI.", + "type": "object", + "properties": { + "width": { + "description": "Current viewport width in pixels.", + "type": "number" }, - { - "anyOf": [ - { - "type": "object", - "properties": { - "width": { - "type": "number" - } - }, - "required": ["width"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "maxWidth": { - "type": "number" - } - }, - "additionalProperties": false - } - ] + "height": { + "description": "Current viewport height in pixels.", + "type": "number" + }, + "maxHeight": { + "description": "Maximum available height in pixels (if constrained).", + "type": "number" + }, + "maxWidth": { + "description": "Maximum available width in pixels (if constrained).", + "type": "number" } - ] + }, + "required": ["width", "height"], + "additionalProperties": false }, "locale": { "description": "User's language and region preference in BCP 47 format.", @@ -1282,55 +1255,28 @@ } }, "viewport": { - "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.", - "allOf": [ - { - "anyOf": [ - { - "type": "object", - "properties": { - "height": { - "type": "number" - } - }, - "required": ["height"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "maxHeight": { - "type": "number" - } - }, - "additionalProperties": false - } - ] + "description": "Current and maximum dimensions available to the UI.", + "type": "object", + "properties": { + "width": { + "description": "Current viewport width in pixels.", + "type": "number" }, - { - "anyOf": [ - { - "type": "object", - "properties": { - "width": { - "type": "number" - } - }, - "required": ["width"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "maxWidth": { - "type": "number" - } - }, - "additionalProperties": false - } - ] + "height": { + "description": "Current viewport height in pixels.", + "type": "number" + }, + "maxHeight": { + "description": "Maximum available height in pixels (if constrained).", + "type": "number" + }, + "maxWidth": { + "description": "Maximum available width in pixels (if constrained).", + "type": "number" } - ] + }, + "required": ["width", "height"], + "additionalProperties": false }, "locale": { "description": "User's language and region preference in BCP 47 format.", @@ -2468,55 +2414,28 @@ } }, "viewport": { - "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.", - "allOf": [ - { - "anyOf": [ - { - "type": "object", - "properties": { - "height": { - "type": "number" - } - }, - "required": ["height"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "maxHeight": { - "type": "number" - } - }, - "additionalProperties": false - } - ] + "description": "Current and maximum dimensions available to the UI.", + "type": "object", + "properties": { + "width": { + "description": "Current viewport width in pixels.", + "type": "number" }, - { - "anyOf": [ - { - "type": "object", - "properties": { - "width": { - "type": "number" - } - }, - "required": ["width"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "maxWidth": { - "type": "number" - } - }, - "additionalProperties": false - } - ] + "height": { + "description": "Current viewport height in pixels.", + "type": "number" + }, + "maxHeight": { + "description": "Maximum available height in pixels (if constrained).", + "type": "number" + }, + "maxWidth": { + "description": "Maximum available width in pixels (if constrained).", + "type": "number" } - ] + }, + "required": ["width", "height"], + "additionalProperties": false }, "locale": { "description": "User's language and region preference in BCP 47 format.", diff --git a/src/generated/schema.ts b/src/generated/schema.ts index 5558961..5e59edc 100644 --- a/src/generated/schema.ts +++ b/src/generated/schema.ts @@ -562,39 +562,26 @@ export const McpUiHostContextSchema = z.looseObject({ .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. - */ + /** @description Current and maximum dimensions available to the UI. */ 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(), - }), - ]), - ) + .object({ + /** @description Current viewport width in pixels. */ + width: z.number().describe("Current viewport width in pixels."), + /** @description Current viewport height in pixels. */ + height: z.number().describe("Current viewport height in pixels."), + /** @description Maximum available height in pixels (if constrained). */ + maxHeight: z + .number() + .optional() + .describe("Maximum available height in pixels (if constrained)."), + /** @description Maximum available width in pixels (if constrained). */ + maxWidth: z + .number() + .optional() + .describe("Maximum available width in pixels (if constrained)."), + }) .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.", - ), + .describe("Current and maximum dimensions available to the UI."), /** @description User's language and region preference in BCP 47 format. */ locale: z .string() diff --git a/src/spec.types.ts b/src/spec.types.ts index 1a0c8c2..f97a0a6 100644 --- a/src/spec.types.ts +++ b/src/spec.types.ts @@ -324,18 +324,17 @@ export interface McpUiHostContext { displayMode?: McpUiDisplayMode; /** @description Display modes the host supports. */ availableDisplayModes?: string[]; - /** - * @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?: ({ height: number } | { maxHeight?: number }) & - ({ width: number } | { maxWidth?: number }); + /** @description Current and maximum dimensions available to the UI. */ + viewport?: { + /** @description Current viewport width in pixels. */ + width: number; + /** @description Current viewport height in pixels. */ + height: number; + /** @description Maximum available height in pixels (if constrained). */ + maxHeight?: number; + /** @description Maximum available width in pixels (if constrained). */ + maxWidth?: number; + }; /** @description User's language and region preference in BCP 47 format. */ locale?: string; /** @description User's timezone in IANA format. */ From 4e01a6c2000f212e8e51b0ee33dffb51c91ea06b Mon Sep 17 00:00:00 2001 From: martinalong Date: Wed, 17 Dec 2025 21:28:54 -0800 Subject: [PATCH 3/8] Make height/width optional as well --- specification/draft/apps.mdx | 12 ++++++------ src/generated/schema.json | 3 --- src/generated/schema.ts | 10 ++++++++-- src/spec.types.ts | 4 ++-- 4 files changed, 16 insertions(+), 13 deletions(-) diff --git a/specification/draft/apps.mdx b/specification/draft/apps.mdx index f4aa9d5..c7d9838 100644 --- a/specification/draft/apps.mdx +++ b/specification/draft/apps.mdx @@ -464,9 +464,9 @@ interface HostContext { /** Current and maximum dimensions available to the UI. */ viewport?: { /** Current viewport width in pixels. */ - width: number; + width?: number; /** Current viewport height in pixels. */ - height: number; + height?: number; /** Maximum available height in pixels (if constrained). */ maxHeight?: number; /** Maximum available width in pixels (if constrained). */ @@ -552,20 +552,20 @@ if (viewport) { // Handle height if ("height" in viewport) { // Fixed height: fill the container - document.body.style.height = "100%"; + document.documentElement.style.height = "100vh"; } else if (viewport.maxHeight) { // Flexible with max: let content determine size, up to max - document.body.style.maxHeight = `${viewport.maxHeight}px`; + document.documentElement.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%"; + document.documentElement.style.width = "100vw"; } else if (viewport.maxWidth) { // Flexible with max: let content determine size, up to max - document.body.style.maxWidth = `${viewport.maxWidth}px`; + document.documentElement.style.maxWidth = `${viewport.maxWidth}px`; } // If neither, width is unbounded } diff --git a/src/generated/schema.json b/src/generated/schema.json index e835986..839e174 100644 --- a/src/generated/schema.json +++ b/src/generated/schema.json @@ -649,7 +649,6 @@ "type": "number" } }, - "required": ["width", "height"], "additionalProperties": false }, "locale": { @@ -1275,7 +1274,6 @@ "type": "number" } }, - "required": ["width", "height"], "additionalProperties": false }, "locale": { @@ -2434,7 +2432,6 @@ "type": "number" } }, - "required": ["width", "height"], "additionalProperties": false }, "locale": { diff --git a/src/generated/schema.ts b/src/generated/schema.ts index 5e59edc..f6deae5 100644 --- a/src/generated/schema.ts +++ b/src/generated/schema.ts @@ -566,9 +566,15 @@ export const McpUiHostContextSchema = z.looseObject({ viewport: z .object({ /** @description Current viewport width in pixels. */ - width: z.number().describe("Current viewport width in pixels."), + width: z + .number() + .optional() + .describe("Current viewport width in pixels."), /** @description Current viewport height in pixels. */ - height: z.number().describe("Current viewport height in pixels."), + height: z + .number() + .optional() + .describe("Current viewport height in pixels."), /** @description Maximum available height in pixels (if constrained). */ maxHeight: z .number() diff --git a/src/spec.types.ts b/src/spec.types.ts index f97a0a6..13ee625 100644 --- a/src/spec.types.ts +++ b/src/spec.types.ts @@ -327,9 +327,9 @@ export interface McpUiHostContext { /** @description Current and maximum dimensions available to the UI. */ viewport?: { /** @description Current viewport width in pixels. */ - width: number; + width?: number; /** @description Current viewport height in pixels. */ - height: number; + height?: number; /** @description Maximum available height in pixels (if constrained). */ maxHeight?: number; /** @description Maximum available width in pixels (if constrained). */ From 06c0e08a1741e6aead754922e58b94aed69eaee7 Mon Sep 17 00:00:00 2001 From: martinalong Date: Wed, 17 Dec 2025 21:36:35 -0800 Subject: [PATCH 4/8] Clarify documentation --- specification/draft/apps.mdx | 10 ++++----- src/generated/schema.json | 42 ++++++++++++++++++------------------ src/generated/schema.ts | 30 ++++++++++++++++---------- src/spec.types.ts | 10 ++++----- 4 files changed, 50 insertions(+), 42 deletions(-) diff --git a/specification/draft/apps.mdx b/specification/draft/apps.mdx index c7d9838..facb348 100644 --- a/specification/draft/apps.mdx +++ b/specification/draft/apps.mdx @@ -463,14 +463,14 @@ interface HostContext { availableDisplayModes?: string[]; /** Current and maximum dimensions available to the UI. */ viewport?: { - /** Current viewport width in pixels. */ + /** Viewport width (if fixed). Only pass width or maxWidth, not both. */ width?: number; - /** Current viewport height in pixels. */ + /** Viewport height (if fixed). Only pass height or maxHeight, not both. */ height?: number; - /** Maximum available height in pixels (if constrained). */ - maxHeight?: number; - /** Maximum available width in pixels (if constrained). */ + /** Maximum available viewport width in pixels (if constrained). */ maxWidth?: number; + /** Maximum available viewport height in pixels (if constrained). */ + maxHeight?: number; }; /** User's language/region preference (BCP 47, e.g., "en-US") */ locale?: string; diff --git a/src/generated/schema.json b/src/generated/schema.json index 839e174..6895edc 100644 --- a/src/generated/schema.json +++ b/src/generated/schema.json @@ -633,19 +633,19 @@ "type": "object", "properties": { "width": { - "description": "Current viewport width in pixels.", + "description": "Viewport width in pixels (if fixed). Only pass width or maxWidth, not both.", "type": "number" }, "height": { - "description": "Current viewport height in pixels.", - "type": "number" - }, - "maxHeight": { - "description": "Maximum available height in pixels (if constrained).", + "description": "Viewport height in pixels (if fixed). Only pass height or maxHeight, not both.", "type": "number" }, "maxWidth": { - "description": "Maximum available width in pixels (if constrained).", + "description": "Maximum available viewport width in pixels (if constrained). Only pass width or maxWidth, not both.", + "type": "number" + }, + "maxHeight": { + "description": "Maximum available viewport height in pixels (if constrained). Only pass height or maxHeight, not both.", "type": "number" } }, @@ -1258,19 +1258,19 @@ "type": "object", "properties": { "width": { - "description": "Current viewport width in pixels.", + "description": "Viewport width in pixels (if fixed). Only pass width or maxWidth, not both.", "type": "number" }, "height": { - "description": "Current viewport height in pixels.", - "type": "number" - }, - "maxHeight": { - "description": "Maximum available height in pixels (if constrained).", + "description": "Viewport height in pixels (if fixed). Only pass height or maxHeight, not both.", "type": "number" }, "maxWidth": { - "description": "Maximum available width in pixels (if constrained).", + "description": "Maximum available viewport width in pixels (if constrained). Only pass width or maxWidth, not both.", + "type": "number" + }, + "maxHeight": { + "description": "Maximum available viewport height in pixels (if constrained). Only pass height or maxHeight, not both.", "type": "number" } }, @@ -2416,19 +2416,19 @@ "type": "object", "properties": { "width": { - "description": "Current viewport width in pixels.", + "description": "Viewport width in pixels (if fixed). Only pass width or maxWidth, not both.", "type": "number" }, "height": { - "description": "Current viewport height in pixels.", - "type": "number" - }, - "maxHeight": { - "description": "Maximum available height in pixels (if constrained).", + "description": "Viewport height in pixels (if fixed). Only pass height or maxHeight, not both.", "type": "number" }, "maxWidth": { - "description": "Maximum available width in pixels (if constrained).", + "description": "Maximum available viewport width in pixels (if constrained). Only pass width or maxWidth, not both.", + "type": "number" + }, + "maxHeight": { + "description": "Maximum available viewport height in pixels (if constrained). Only pass height or maxHeight, not both.", "type": "number" } }, diff --git a/src/generated/schema.ts b/src/generated/schema.ts index f6deae5..2c73ad6 100644 --- a/src/generated/schema.ts +++ b/src/generated/schema.ts @@ -565,26 +565,34 @@ export const McpUiHostContextSchema = z.looseObject({ /** @description Current and maximum dimensions available to the UI. */ viewport: z .object({ - /** @description Current viewport width in pixels. */ + /** @description Viewport width in pixels (if fixed). Only pass width or maxWidth, not both. */ width: z .number() .optional() - .describe("Current viewport width in pixels."), - /** @description Current viewport height in pixels. */ + .describe( + "Viewport width in pixels (if fixed). Only pass width or maxWidth, not both.", + ), + /** @description Viewport height in pixels (if fixed). Only pass height or maxHeight, not both. */ height: z .number() .optional() - .describe("Current viewport height in pixels."), - /** @description Maximum available height in pixels (if constrained). */ - maxHeight: z - .number() - .optional() - .describe("Maximum available height in pixels (if constrained)."), - /** @description Maximum available width in pixels (if constrained). */ + .describe( + "Viewport height in pixels (if fixed). Only pass height or maxHeight, not both.", + ), + /** @description Maximum available viewport width in pixels (if constrained). Only pass width or maxWidth, not both.*/ maxWidth: z .number() .optional() - .describe("Maximum available width in pixels (if constrained)."), + .describe( + "Maximum available viewport width in pixels (if constrained). Only pass width or maxWidth, not both.", + ), + /** @description Maximum available viewport height in pixels (if constrained). Only pass height or maxHeight, not both. */ + maxHeight: z + .number() + .optional() + .describe( + "Maximum available viewport height in pixels (if constrained). Only pass height or maxHeight, not both.", + ), }) .optional() .describe("Current and maximum dimensions available to the UI."), diff --git a/src/spec.types.ts b/src/spec.types.ts index 13ee625..8f04e7e 100644 --- a/src/spec.types.ts +++ b/src/spec.types.ts @@ -326,14 +326,14 @@ export interface McpUiHostContext { availableDisplayModes?: string[]; /** @description Current and maximum dimensions available to the UI. */ viewport?: { - /** @description Current viewport width in pixels. */ + /** @description Viewport width in pixels (if fixed). Only pass width or maxWidth, not both. */ width?: number; - /** @description Current viewport height in pixels. */ + /** @description Viewport height in pixels (if fixed). Only pass height or maxHeight, not both. */ height?: number; - /** @description Maximum available height in pixels (if constrained). */ - maxHeight?: number; - /** @description Maximum available width in pixels (if constrained). */ + /** @description Maximum available viewport width in pixels (if constrained). Only pass width or maxWidth, not both.*/ maxWidth?: number; + /** @description Maximum available viewport height in pixels (if constrained). Only pass height or maxHeight, not both. */ + maxHeight?: number; }; /** @description User's language and region preference in BCP 47 format. */ locale?: string; From c22e86233b8fab3dfc1bf1e5605df6de1ab5e1ac Mon Sep 17 00:00:00 2001 From: martinalong Date: Thu, 18 Dec 2025 00:01:34 -0800 Subject: [PATCH 5/8] Allow undefined values --- src/generated/schema.json | 108 +++++++++++++++++++++++++++++++++----- src/generated/schema.ts | 9 ++-- src/spec.types.ts | 9 ++-- 3 files changed, 106 insertions(+), 20 deletions(-) diff --git a/src/generated/schema.json b/src/generated/schema.json index 6895edc..fed9bb7 100644 --- a/src/generated/schema.json +++ b/src/generated/schema.json @@ -330,6 +330,10 @@ "type": "string", "const": "--color-text-inverse" }, + { + "type": "string", + "const": "--color-text-ghost" + }, { "type": "string", "const": "--color-text-info" @@ -634,19 +638,39 @@ "properties": { "width": { "description": "Viewport width in pixels (if fixed). Only pass width or maxWidth, not both.", - "type": "number" + "anyOf": [ + { + "type": "number" + }, + {} + ] }, "height": { "description": "Viewport height in pixels (if fixed). Only pass height or maxHeight, not both.", - "type": "number" + "anyOf": [ + { + "type": "number" + }, + {} + ] }, "maxWidth": { "description": "Maximum available viewport width in pixels (if constrained). Only pass width or maxWidth, not both.", - "type": "number" + "anyOf": [ + { + "type": "number" + }, + {} + ] }, "maxHeight": { "description": "Maximum available viewport height in pixels (if constrained). Only pass height or maxHeight, not both.", - "type": "number" + "anyOf": [ + { + "type": "number" + }, + {} + ] } }, "additionalProperties": false @@ -955,6 +979,10 @@ "type": "string", "const": "--color-text-inverse" }, + { + "type": "string", + "const": "--color-text-ghost" + }, { "type": "string", "const": "--color-text-info" @@ -1259,19 +1287,39 @@ "properties": { "width": { "description": "Viewport width in pixels (if fixed). Only pass width or maxWidth, not both.", - "type": "number" + "anyOf": [ + { + "type": "number" + }, + {} + ] }, "height": { "description": "Viewport height in pixels (if fixed). Only pass height or maxHeight, not both.", - "type": "number" + "anyOf": [ + { + "type": "number" + }, + {} + ] }, "maxWidth": { "description": "Maximum available viewport width in pixels (if constrained). Only pass width or maxWidth, not both.", - "type": "number" + "anyOf": [ + { + "type": "number" + }, + {} + ] }, "maxHeight": { "description": "Maximum available viewport height in pixels (if constrained). Only pass height or maxHeight, not both.", - "type": "number" + "anyOf": [ + { + "type": "number" + }, + {} + ] } }, "additionalProperties": false @@ -1424,6 +1472,10 @@ "type": "string", "const": "--color-text-inverse" }, + { + "type": "string", + "const": "--color-text-ghost" + }, { "type": "string", "const": "--color-text-info" @@ -2113,6 +2165,10 @@ "type": "string", "const": "--color-text-inverse" }, + { + "type": "string", + "const": "--color-text-ghost" + }, { "type": "string", "const": "--color-text-info" @@ -2417,19 +2473,39 @@ "properties": { "width": { "description": "Viewport width in pixels (if fixed). Only pass width or maxWidth, not both.", - "type": "number" + "anyOf": [ + { + "type": "number" + }, + {} + ] }, "height": { "description": "Viewport height in pixels (if fixed). Only pass height or maxHeight, not both.", - "type": "number" + "anyOf": [ + { + "type": "number" + }, + {} + ] }, "maxWidth": { "description": "Maximum available viewport width in pixels (if constrained). Only pass width or maxWidth, not both.", - "type": "number" + "anyOf": [ + { + "type": "number" + }, + {} + ] }, "maxHeight": { "description": "Maximum available viewport height in pixels (if constrained). Only pass height or maxHeight, not both.", - "type": "number" + "anyOf": [ + { + "type": "number" + }, + {} + ] } }, "additionalProperties": false @@ -3207,6 +3283,10 @@ "type": "string", "const": "--color-text-inverse" }, + { + "type": "string", + "const": "--color-text-ghost" + }, { "type": "string", "const": "--color-text-info" @@ -3520,6 +3600,10 @@ "type": "string", "const": "--color-text-inverse" }, + { + "type": "string", + "const": "--color-text-ghost" + }, { "type": "string", "const": "--color-text-info" diff --git a/src/generated/schema.ts b/src/generated/schema.ts index 2c73ad6..16c0895 100644 --- a/src/generated/schema.ts +++ b/src/generated/schema.ts @@ -43,6 +43,7 @@ export const McpUiStyleVariableKeySchema = z z.literal("--color-text-secondary"), z.literal("--color-text-tertiary"), z.literal("--color-text-inverse"), + z.literal("--color-text-ghost"), z.literal("--color-text-info"), z.literal("--color-text-danger"), z.literal("--color-text-success"), @@ -567,28 +568,28 @@ export const McpUiHostContextSchema = z.looseObject({ .object({ /** @description Viewport width in pixels (if fixed). Only pass width or maxWidth, not both. */ width: z - .number() + .union([z.number(), z.undefined()]) .optional() .describe( "Viewport width in pixels (if fixed). Only pass width or maxWidth, not both.", ), /** @description Viewport height in pixels (if fixed). Only pass height or maxHeight, not both. */ height: z - .number() + .union([z.number(), z.undefined()]) .optional() .describe( "Viewport height in pixels (if fixed). Only pass height or maxHeight, not both.", ), /** @description Maximum available viewport width in pixels (if constrained). Only pass width or maxWidth, not both.*/ maxWidth: z - .number() + .union([z.number(), z.undefined()]) .optional() .describe( "Maximum available viewport width in pixels (if constrained). Only pass width or maxWidth, not both.", ), /** @description Maximum available viewport height in pixels (if constrained). Only pass height or maxHeight, not both. */ maxHeight: z - .number() + .union([z.number(), z.undefined()]) .optional() .describe( "Maximum available viewport height in pixels (if constrained). Only pass height or maxHeight, not both.", diff --git a/src/spec.types.ts b/src/spec.types.ts index 8f04e7e..1d03819 100644 --- a/src/spec.types.ts +++ b/src/spec.types.ts @@ -56,6 +56,7 @@ export type McpUiStyleVariableKey = | "--color-text-secondary" | "--color-text-tertiary" | "--color-text-inverse" + | "--color-text-ghost" | "--color-text-info" | "--color-text-danger" | "--color-text-success" @@ -327,13 +328,13 @@ export interface McpUiHostContext { /** @description Current and maximum dimensions available to the UI. */ viewport?: { /** @description Viewport width in pixels (if fixed). Only pass width or maxWidth, not both. */ - width?: number; + width?: number | undefined; /** @description Viewport height in pixels (if fixed). Only pass height or maxHeight, not both. */ - height?: number; + height?: number | undefined; /** @description Maximum available viewport width in pixels (if constrained). Only pass width or maxWidth, not both.*/ - maxWidth?: number; + maxWidth?: number | undefined; /** @description Maximum available viewport height in pixels (if constrained). Only pass height or maxHeight, not both. */ - maxHeight?: number; + maxHeight?: number | undefined; }; /** @description User's language and region preference in BCP 47 format. */ locale?: string; From 82e64d56bb59a8a14a7eaddebe3077d96a7ed05c Mon Sep 17 00:00:00 2001 From: martinalong Date: Fri, 26 Dec 2025 14:04:05 -0800 Subject: [PATCH 6/8] Separate viewport and containerDimensions --- specification/draft/apps.mdx | 59 +++++--- src/app-bridge.test.ts | 16 ++- src/app-bridge.ts | 3 +- src/generated/schema.json | 267 ++++++++++++++++++++++++++--------- src/generated/schema.ts | 72 ++++++---- src/spec.types.ts | 37 ++++- 6 files changed, 332 insertions(+), 122 deletions(-) diff --git a/specification/draft/apps.mdx b/specification/draft/apps.mdx index facb348..51898a1 100644 --- a/specification/draft/apps.mdx +++ b/specification/draft/apps.mdx @@ -461,16 +461,20 @@ interface HostContext { displayMode?: "inline" | "fullscreen" | "pip"; /** Display modes the host supports */ availableDisplayModes?: string[]; - /** Current and maximum dimensions available to the UI. */ + /** Container dimensions for the iframe. Specify either width or maxWidth, and either height or maxHeight. */ + containerDimensions?: ( + | { height: number } // If specified, container is fixed at this height + | { maxHeight?: number } // Otherwise, container height is determined by the UI height, up to this maximum height (if defined) + ) & ( + | { width: number } // If specified, container is fixed at this width + | { maxWidth?: number } // Otherwise, container width is determined by the UI width, up to this maximum width (if defined) + ); + /** Host window viewport dimensions */ viewport?: { - /** Viewport width (if fixed). Only pass width or maxWidth, not both. */ + /** Window viewport width in pixels. */ width?: number; - /** Viewport height (if fixed). Only pass height or maxHeight, not both. */ + /** Window viewport height in pixels. */ height?: number; - /** Maximum available viewport width in pixels (if constrained). */ - maxWidth?: number; - /** Maximum available viewport height in pixels (if constrained). */ - maxHeight?: number; }; /** User's language/region preference (BCP 47, e.g., "en-US") */ locale?: string; @@ -520,20 +524,25 @@ Example: } }, "displayMode": "inline", - "viewport": { "width": 400, "maxHeight": 600 } + "containerDimensions": { "width": 400, "maxHeight": 600 } + "viewport": { "width": 1920, "height": 1080 }, } } } ``` -### Viewport and Sizing +### Viewport and Dimensions -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**. +The `HostContext` provides two separate fields for sizing information: -#### Viewport Modes +- **`containerDimensions`**: The dimensions of the container that holds the app. This controls the actual space the app occupies within the host. Each dimension (height and width) operates independently and can be either **fixed** or **flexible**. -| Mode | Viewport Field | Meaning | -|------|---------------|---------| +- **`viewport`**: The host window's dimensions (e.g., `window.innerWidth` and `window.innerHeight`). Apps can use this to make responsive layout decisions based on the overall screen size. + +#### Dimension Modes + +| Mode | Dimensions 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. | @@ -542,33 +551,39 @@ These modes can be combined independently. For example, a host might specify a f #### App Behavior -Apps should check the viewport configuration and apply appropriate CSS: +Apps should check the containerDimensions configuration and apply appropriate CSS: ```typescript // In the app's initialization -const viewport = hostContext.viewport; +const containerDimensions = hostContext.containerDimensions; -if (viewport) { +if (containerDimensions) { // Handle height - if ("height" in viewport) { + if ("height" in containerDimensions) { // Fixed height: fill the container document.documentElement.style.height = "100vh"; - } else if (viewport.maxHeight) { + } else if ("maxHeight" in containerDimensions && containerDimensions.maxHeight) { // Flexible with max: let content determine size, up to max - document.documentElement.style.maxHeight = `${viewport.maxHeight}px`; + document.documentElement.style.maxHeight = `${containerDimensions.maxHeight}px`; } // If neither, height is unbounded // Handle width - if ("width" in viewport) { + if ("width" in containerDimensions) { // Fixed width: fill the container document.documentElement.style.width = "100vw"; - } else if (viewport.maxWidth) { + } else if ("maxWidth" in containerDimensions && containerDimensions.maxWidth) { // Flexible with max: let content determine size, up to max - document.documentElement.style.maxWidth = `${viewport.maxWidth}px`; + document.documentElement.style.maxWidth = `${containerDimensions.maxWidth}px`; } // If neither, width is unbounded } + +// Apps can also use viewport for additional data to make responsive layout decisions +const viewport = hostContext.viewport; +if (viewport?.width && viewport.width < 768) { + // Apply mobile-friendly layout +} ``` #### Host Behavior diff --git a/src/app-bridge.test.ts b/src/app-bridge.test.ts index 5a36b47..ce3a338 100644 --- a/src/app-bridge.test.ts +++ b/src/app-bridge.test.ts @@ -113,7 +113,8 @@ describe("App <-> AppBridge integration", () => { const testHostContext = { theme: "dark" as const, locale: "en-US", - viewport: { width: 800, height: 600, maxHeight: 600 }, + viewport: { width: 800, height: 600 }, + containerDimensions: { width: 800, maxHeight: 600 }, }; const newBridge = new AppBridge( createMockClient() as Client, @@ -337,7 +338,8 @@ describe("App <-> AppBridge integration", () => { const initialContext = { theme: "light" as const, locale: "en-US", - viewport: { width: 800, height: 600, maxHeight: 600 }, + viewport: { width: 800, height: 600 }, + containerDimensions: { width: 800, maxHeight: 600 }, }; const newBridge = new AppBridge( createMockClient() as Client, @@ -354,22 +356,26 @@ describe("App <-> AppBridge integration", () => { newBridge.sendHostContextChange({ theme: "dark" }); await flush(); - // Send another partial update: only viewport changes + // Send another partial update: only viewport and containerDimensions change newBridge.sendHostContextChange({ - viewport: { width: 1024, height: 768, maxHeight: 768 }, + viewport: { width: 1024, height: 768 }, + containerDimensions: { width: 1024, maxHeight: 768 }, }); await flush(); // getHostContext should have accumulated all updates: // - locale from initial (unchanged) // - theme from first partial update - // - viewport from second partial update + // - viewport and containerDimensions from second partial update const context = newApp.getHostContext(); expect(context?.theme).toBe("dark"); expect(context?.locale).toBe("en-US"); expect(context?.viewport).toEqual({ width: 1024, height: 768, + }); + expect(context?.containerDimensions).toEqual({ + width: 1024, maxHeight: 768, }); diff --git a/src/app-bridge.ts b/src/app-bridge.ts index 111be34..8a06f9e 100644 --- a/src/app-bridge.ts +++ b/src/app-bridge.ts @@ -1008,7 +1008,8 @@ export class AppBridge extends Protocol< * ```typescript * bridge.setHostContext({ * theme: "dark", - * viewport: { width: 800, maxHeight: 600 } + * viewport: { width: 800, height: 600 }, + * containerDimensions: { maxHeight: 600, width: 800 } * }); * ``` * diff --git a/src/generated/schema.json b/src/generated/schema.json index fed9bb7..a5eb424 100644 --- a/src/generated/schema.json +++ b/src/generated/schema.json @@ -632,12 +632,77 @@ "type": "string" } }, + "containerDimensions": { + "description": "Container dimensions. Represents the dimensions of the iframe or other\ncontainer holding the app. Specify either width or maxWidth, and either height or maxHeight.", + "allOf": [ + { + "anyOf": [ + { + "type": "object", + "properties": { + "height": { + "description": "Fixed container height in pixels.", + "type": "number" + } + }, + "required": ["height"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "maxHeight": { + "description": "Maximum container height in pixels.", + "anyOf": [ + { + "type": "number" + }, + {} + ] + } + }, + "additionalProperties": false + } + ] + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "width": { + "description": "Fixed container width in pixels.", + "type": "number" + } + }, + "required": ["width"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "maxWidth": { + "description": "Maximum container width in pixels.", + "anyOf": [ + { + "type": "number" + }, + {} + ] + } + }, + "additionalProperties": false + } + ] + } + ] + }, "viewport": { - "description": "Current and maximum dimensions available to the UI.", + "description": "Window viewport dimensions. Represents the host window's viewport size,\nwhich provides additional information apps can use to make responsive layout decisions.", "type": "object", "properties": { "width": { - "description": "Viewport width in pixels (if fixed). Only pass width or maxWidth, not both.", + "description": "Window viewport width in pixels.", "anyOf": [ { "type": "number" @@ -646,25 +711,7 @@ ] }, "height": { - "description": "Viewport height in pixels (if fixed). Only pass height or maxHeight, not both.", - "anyOf": [ - { - "type": "number" - }, - {} - ] - }, - "maxWidth": { - "description": "Maximum available viewport width in pixels (if constrained). Only pass width or maxWidth, not both.", - "anyOf": [ - { - "type": "number" - }, - {} - ] - }, - "maxHeight": { - "description": "Maximum available viewport height in pixels (if constrained). Only pass height or maxHeight, not both.", + "description": "Window viewport height in pixels.", "anyOf": [ { "type": "number" @@ -1281,12 +1328,77 @@ "type": "string" } }, + "containerDimensions": { + "description": "Container dimensions. Represents the dimensions of the iframe or other\ncontainer holding the app. Specify either width or maxWidth, and either height or maxHeight.", + "allOf": [ + { + "anyOf": [ + { + "type": "object", + "properties": { + "height": { + "description": "Fixed container height in pixels.", + "type": "number" + } + }, + "required": ["height"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "maxHeight": { + "description": "Maximum container height in pixels.", + "anyOf": [ + { + "type": "number" + }, + {} + ] + } + }, + "additionalProperties": false + } + ] + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "width": { + "description": "Fixed container width in pixels.", + "type": "number" + } + }, + "required": ["width"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "maxWidth": { + "description": "Maximum container width in pixels.", + "anyOf": [ + { + "type": "number" + }, + {} + ] + } + }, + "additionalProperties": false + } + ] + } + ] + }, "viewport": { - "description": "Current and maximum dimensions available to the UI.", + "description": "Window viewport dimensions. Represents the host window's viewport size,\nwhich provides additional information apps can use to make responsive layout decisions.", "type": "object", "properties": { "width": { - "description": "Viewport width in pixels (if fixed). Only pass width or maxWidth, not both.", + "description": "Window viewport width in pixels.", "anyOf": [ { "type": "number" @@ -1295,25 +1407,7 @@ ] }, "height": { - "description": "Viewport height in pixels (if fixed). Only pass height or maxHeight, not both.", - "anyOf": [ - { - "type": "number" - }, - {} - ] - }, - "maxWidth": { - "description": "Maximum available viewport width in pixels (if constrained). Only pass width or maxWidth, not both.", - "anyOf": [ - { - "type": "number" - }, - {} - ] - }, - "maxHeight": { - "description": "Maximum available viewport height in pixels (if constrained). Only pass height or maxHeight, not both.", + "description": "Window viewport height in pixels.", "anyOf": [ { "type": "number" @@ -2467,12 +2561,77 @@ "type": "string" } }, + "containerDimensions": { + "description": "Container dimensions. Represents the dimensions of the iframe or other\ncontainer holding the app. Specify either width or maxWidth, and either height or maxHeight.", + "allOf": [ + { + "anyOf": [ + { + "type": "object", + "properties": { + "height": { + "description": "Fixed container height in pixels.", + "type": "number" + } + }, + "required": ["height"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "maxHeight": { + "description": "Maximum container height in pixels.", + "anyOf": [ + { + "type": "number" + }, + {} + ] + } + }, + "additionalProperties": false + } + ] + }, + { + "anyOf": [ + { + "type": "object", + "properties": { + "width": { + "description": "Fixed container width in pixels.", + "type": "number" + } + }, + "required": ["width"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "maxWidth": { + "description": "Maximum container width in pixels.", + "anyOf": [ + { + "type": "number" + }, + {} + ] + } + }, + "additionalProperties": false + } + ] + } + ] + }, "viewport": { - "description": "Current and maximum dimensions available to the UI.", + "description": "Window viewport dimensions. Represents the host window's viewport size,\nwhich provides additional information apps can use to make responsive layout decisions.", "type": "object", "properties": { "width": { - "description": "Viewport width in pixels (if fixed). Only pass width or maxWidth, not both.", + "description": "Window viewport width in pixels.", "anyOf": [ { "type": "number" @@ -2481,25 +2640,7 @@ ] }, "height": { - "description": "Viewport height in pixels (if fixed). Only pass height or maxHeight, not both.", - "anyOf": [ - { - "type": "number" - }, - {} - ] - }, - "maxWidth": { - "description": "Maximum available viewport width in pixels (if constrained). Only pass width or maxWidth, not both.", - "anyOf": [ - { - "type": "number" - }, - {} - ] - }, - "maxHeight": { - "description": "Maximum available viewport height in pixels (if constrained). Only pass height or maxHeight, not both.", + "description": "Window viewport height in pixels.", "anyOf": [ { "type": "number" diff --git a/src/generated/schema.ts b/src/generated/schema.ts index 16c0895..3abd30c 100644 --- a/src/generated/schema.ts +++ b/src/generated/schema.ts @@ -563,40 +563,64 @@ export const McpUiHostContextSchema = z.looseObject({ .array(z.string()) .optional() .describe("Display modes the host supports."), - /** @description Current and maximum dimensions available to the UI. */ + /** + * @description Container dimensions. Represents the dimensions of the iframe or other + * container holding the app. Specify either width or maxWidth, and either height or maxHeight. + */ + containerDimensions: z + .union([ + z.object({ + /** @description Fixed container height in pixels. */ + height: z.number().describe("Fixed container height in pixels."), + }), + z.object({ + /** @description Maximum container height in pixels. */ + maxHeight: z + .union([z.number(), z.undefined()]) + .optional() + .describe("Maximum container height in pixels."), + }), + ]) + .and( + z.union([ + z.object({ + /** @description Fixed container width in pixels. */ + width: z.number().describe("Fixed container width in pixels."), + }), + z.object({ + /** @description Maximum container width in pixels. */ + maxWidth: z + .union([z.number(), z.undefined()]) + .optional() + .describe("Maximum container width in pixels."), + }), + ]), + ) + .optional() + .describe( + "Container dimensions. Represents the dimensions of the iframe or other\ncontainer holding the app. Specify either width or maxWidth, and either height or maxHeight.", + ), + /** + * @description Window viewport dimensions. Represents the host window's viewport size, + * which provides additional information apps can use to make responsive layout decisions. + */ viewport: z .object({ - /** @description Viewport width in pixels (if fixed). Only pass width or maxWidth, not both. */ + /** @description Window viewport width in pixels. */ width: z .union([z.number(), z.undefined()]) .optional() - .describe( - "Viewport width in pixels (if fixed). Only pass width or maxWidth, not both.", - ), - /** @description Viewport height in pixels (if fixed). Only pass height or maxHeight, not both. */ + .describe("Window viewport width in pixels."), + /** @description Window viewport height in pixels. */ height: z .union([z.number(), z.undefined()]) .optional() - .describe( - "Viewport height in pixels (if fixed). Only pass height or maxHeight, not both.", - ), - /** @description Maximum available viewport width in pixels (if constrained). Only pass width or maxWidth, not both.*/ - maxWidth: z - .union([z.number(), z.undefined()]) - .optional() - .describe( - "Maximum available viewport width in pixels (if constrained). Only pass width or maxWidth, not both.", - ), - /** @description Maximum available viewport height in pixels (if constrained). Only pass height or maxHeight, not both. */ - maxHeight: z - .union([z.number(), z.undefined()]) - .optional() - .describe( - "Maximum available viewport height in pixels (if constrained). Only pass height or maxHeight, not both.", - ), + .describe("Window viewport height in pixels."), }) .optional() - .describe("Current and maximum dimensions available to the UI."), + .describe( + "Window viewport dimensions. Represents the host window's viewport size,\nwhich provides additional information apps can use to make responsive layout decisions.", + ), /** @description User's language and region preference in BCP 47 format. */ locale: z .string() diff --git a/src/spec.types.ts b/src/spec.types.ts index 1d03819..57627e7 100644 --- a/src/spec.types.ts +++ b/src/spec.types.ts @@ -325,16 +325,39 @@ export interface McpUiHostContext { displayMode?: McpUiDisplayMode; /** @description Display modes the host supports. */ availableDisplayModes?: string[]; - /** @description Current and maximum dimensions available to the UI. */ + /** + * @description Container dimensions. Represents the dimensions of the iframe or other + * container holding the app. Specify either width or maxWidth, and either height or maxHeight. + */ + containerDimensions?: ( + | { + /** @description Fixed container height in pixels. */ + height: number; + } + | { + /** @description Maximum container height in pixels. */ + maxHeight?: number | undefined; + } + ) & + ( + | { + /** @description Fixed container width in pixels. */ + width: number; + } + | { + /** @description Maximum container width in pixels. */ + maxWidth?: number | undefined; + } + ); + /** + * @description Window viewport dimensions. Represents the host window's viewport size, + * which provides additional information apps can use to make responsive layout decisions. + */ viewport?: { - /** @description Viewport width in pixels (if fixed). Only pass width or maxWidth, not both. */ + /** @description Window viewport width in pixels. */ width?: number | undefined; - /** @description Viewport height in pixels (if fixed). Only pass height or maxHeight, not both. */ + /** @description Window viewport height in pixels. */ height?: number | undefined; - /** @description Maximum available viewport width in pixels (if constrained). Only pass width or maxWidth, not both.*/ - maxWidth?: number | undefined; - /** @description Maximum available viewport height in pixels (if constrained). Only pass height or maxHeight, not both. */ - maxHeight?: number | undefined; }; /** @description User's language and region preference in BCP 47 format. */ locale?: string; From e90c7945bcf5c1ccefb3ba4e7b6e7d8ad0151750 Mon Sep 17 00:00:00 2001 From: martinalong Date: Fri, 26 Dec 2025 14:16:26 -0800 Subject: [PATCH 7/8] Rebased schemas --- src/generated/schema.ts | 256 ++++++++++++++++++++-------------------- 1 file changed, 129 insertions(+), 127 deletions(-) diff --git a/src/generated/schema.ts b/src/generated/schema.ts index 3abd30c..dfaafa5 100644 --- a/src/generated/schema.ts +++ b/src/generated/schema.ts @@ -533,139 +533,141 @@ export const McpUiToolResultNotificationSchema = z.object({ /** * @description Rich context about the host environment provided to Guest UIs. */ -export const McpUiHostContextSchema = z.looseObject({ - /** @description Metadata of the tool call that instantiated this App. */ - toolInfo: z - .object({ - /** @description 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. */ - tool: ToolSchema.describe( - "Tool definition including name, inputSchema, etc.", - ), - }) - .optional() - .describe("Metadata of the tool call that instantiated this App."), - /** @description Current color theme preference. */ - theme: McpUiThemeSchema.optional().describe( - "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 Container dimensions. Represents the dimensions of the iframe or other - * container holding the app. Specify either width or maxWidth, and either height or maxHeight. - */ - containerDimensions: z - .union([ - z.object({ - /** @description Fixed container height in pixels. */ - height: z.number().describe("Fixed container height in pixels."), - }), - z.object({ - /** @description Maximum container height in pixels. */ - maxHeight: z - .union([z.number(), z.undefined()]) - .optional() - .describe("Maximum container height in pixels."), - }), - ]) - .and( - z.union([ +export const McpUiHostContextSchema = z + .object({ + /** @description Metadata of the tool call that instantiated this App. */ + toolInfo: z + .object({ + /** @description 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. */ + tool: ToolSchema.describe( + "Tool definition including name, inputSchema, etc.", + ), + }) + .optional() + .describe("Metadata of the tool call that instantiated this App."), + /** @description Current color theme preference. */ + theme: McpUiThemeSchema.optional().describe( + "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 Container dimensions. Represents the dimensions of the iframe or other + * container holding the app. Specify either width or maxWidth, and either height or maxHeight. + */ + containerDimensions: z + .union([ z.object({ - /** @description Fixed container width in pixels. */ - width: z.number().describe("Fixed container width in pixels."), + /** @description Fixed container height in pixels. */ + height: z.number().describe("Fixed container height in pixels."), }), z.object({ - /** @description Maximum container width in pixels. */ - maxWidth: z + /** @description Maximum container height in pixels. */ + maxHeight: z .union([z.number(), z.undefined()]) .optional() - .describe("Maximum container width in pixels."), + .describe("Maximum container height in pixels."), }), - ]), - ) - .optional() - .describe( - "Container dimensions. Represents the dimensions of the iframe or other\ncontainer holding the app. Specify either width or maxWidth, and either height or maxHeight.", - ), - /** - * @description Window viewport dimensions. Represents the host window's viewport size, - * which provides additional information apps can use to make responsive layout decisions. - */ - viewport: z - .object({ - /** @description Window viewport width in pixels. */ - width: z - .union([z.number(), z.undefined()]) - .optional() - .describe("Window viewport width in pixels."), - /** @description Window viewport height in pixels. */ - height: z - .union([z.number(), z.undefined()]) - .optional() - .describe("Window viewport height in pixels."), - }) - .optional() - .describe( - "Window viewport dimensions. Represents the host window's viewport size,\nwhich provides additional information apps can use to make responsive layout decisions.", - ), - /** @description User's language and region preference in BCP 47 format. */ - locale: z - .string() - .optional() - .describe("User's language and region preference in BCP 47 format."), - /** @description User's timezone in IANA format. */ - timeZone: z.string().optional().describe("User's timezone in IANA format."), - /** @description Host application identifier. */ - userAgent: z.string().optional().describe("Host application identifier."), - /** @description Platform type for responsive design decisions. */ - platform: z - .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."), -}); + ]) + .and( + z.union([ + z.object({ + /** @description Fixed container width in pixels. */ + width: z.number().describe("Fixed container width in pixels."), + }), + z.object({ + /** @description Maximum container width in pixels. */ + maxWidth: z + .union([z.number(), z.undefined()]) + .optional() + .describe("Maximum container width in pixels."), + }), + ]), + ) + .optional() + .describe( + "Container dimensions. Represents the dimensions of the iframe or other\ncontainer holding the app. Specify either width or maxWidth, and either height or maxHeight.", + ), + /** + * @description Window viewport dimensions. Represents the host window's viewport size, + * which provides additional information apps can use to make responsive layout decisions. + */ + viewport: z + .object({ + /** @description Window viewport width in pixels. */ + width: z + .union([z.number(), z.undefined()]) + .optional() + .describe("Window viewport width in pixels."), + /** @description Window viewport height in pixels. */ + height: z + .union([z.number(), z.undefined()]) + .optional() + .describe("Window viewport height in pixels."), + }) + .optional() + .describe( + "Window viewport dimensions. Represents the host window's viewport size,\nwhich provides additional information apps can use to make responsive layout decisions.", + ), + /** @description User's language and region preference in BCP 47 format. */ + locale: z + .string() + .optional() + .describe("User's language and region preference in BCP 47 format."), + /** @description User's timezone in IANA format. */ + timeZone: z.string().optional().describe("User's timezone in IANA format."), + /** @description Host application identifier. */ + userAgent: z.string().optional().describe("Host application identifier."), + /** @description Platform type for responsive design decisions. */ + platform: z + .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). From dc226be962b3cfe4e4e1567de7e916b1bfaf39bc Mon Sep 17 00:00:00 2001 From: martinalong Date: Wed, 31 Dec 2025 21:36:29 -0800 Subject: [PATCH 8/8] Remove viewport --- .../threejs-server/src/mcp-app-wrapper.tsx | 4 +- specification/draft/apps.mdx | 20 +---- src/app-bridge.test.ts | 11 +-- src/app-bridge.ts | 3 +- src/app.ts | 12 +-- src/generated/schema.json | 75 ------------------- src/generated/schema.ts | 21 ------ src/spec.types.ts | 10 --- 8 files changed, 13 insertions(+), 143 deletions(-) diff --git a/examples/threejs-server/src/mcp-app-wrapper.tsx b/examples/threejs-server/src/mcp-app-wrapper.tsx index 8d46e18..42df752 100644 --- a/examples/threejs-server/src/mcp-app-wrapper.tsx +++ b/examples/threejs-server/src/mcp-app-wrapper.tsx @@ -25,7 +25,7 @@ export interface WidgetProps> { toolInputsPartial: TToolInput | null; /** Tool execution result from the server */ toolResult: CallToolResult | null; - /** Host context (theme, viewport, locale, etc.) */ + /** Host context (theme, dimensions, locale, etc.) */ hostContext: McpUiHostContext | null; /** Call a tool on the MCP server */ callServerTool: App["callServerTool"]; @@ -65,7 +65,7 @@ function McpAppWrapper() { app.ontoolresult = (params) => { setToolResult(params as CallToolResult); }; - // Host context changes (theme, viewport, etc.) + // Host context changes (theme, dimensions, etc.) app.onhostcontextchanged = (params) => { setHostContext(params); }; diff --git a/specification/draft/apps.mdx b/specification/draft/apps.mdx index 51898a1..8305003 100644 --- a/specification/draft/apps.mdx +++ b/specification/draft/apps.mdx @@ -469,13 +469,6 @@ interface HostContext { | { width: number } // If specified, container is fixed at this width | { maxWidth?: number } // Otherwise, container width is determined by the UI width, up to this maximum width (if defined) ); - /** Host window viewport dimensions */ - viewport?: { - /** Window viewport width in pixels. */ - width?: number; - /** Window viewport height in pixels. */ - height?: number; - }; /** User's language/region preference (BCP 47, e.g., "en-US") */ locale?: string; /** User's timezone (IANA, e.g., "America/New_York") */ @@ -525,20 +518,17 @@ Example: }, "displayMode": "inline", "containerDimensions": { "width": 400, "maxHeight": 600 } - "viewport": { "width": 1920, "height": 1080 }, } } } ``` -### Viewport and Dimensions +### Container Dimensions -The `HostContext` provides two separate fields for sizing information: +The `HostContext` provides sizing information via `containerDimensions`: - **`containerDimensions`**: The dimensions of the container that holds the app. This controls the actual space the app occupies within the host. Each dimension (height and width) operates independently and can be either **fixed** or **flexible**. -- **`viewport`**: The host window's dimensions (e.g., `window.innerWidth` and `window.innerHeight`). Apps can use this to make responsive layout decisions based on the overall screen size. - #### Dimension Modes | Mode | Dimensions Field | Meaning | @@ -578,12 +568,6 @@ if (containerDimensions) { } // If neither, width is unbounded } - -// Apps can also use viewport for additional data to make responsive layout decisions -const viewport = hostContext.viewport; -if (viewport?.width && viewport.width < 768) { - // Apply mobile-friendly layout -} ``` #### Host Behavior diff --git a/src/app-bridge.test.ts b/src/app-bridge.test.ts index ce3a338..1e55f6b 100644 --- a/src/app-bridge.test.ts +++ b/src/app-bridge.test.ts @@ -113,7 +113,6 @@ describe("App <-> AppBridge integration", () => { const testHostContext = { theme: "dark" as const, locale: "en-US", - viewport: { width: 800, height: 600 }, containerDimensions: { width: 800, maxHeight: 600 }, }; const newBridge = new AppBridge( @@ -338,7 +337,6 @@ describe("App <-> AppBridge integration", () => { const initialContext = { theme: "light" as const, locale: "en-US", - viewport: { width: 800, height: 600 }, containerDimensions: { width: 800, maxHeight: 600 }, }; const newBridge = new AppBridge( @@ -356,9 +354,8 @@ describe("App <-> AppBridge integration", () => { newBridge.sendHostContextChange({ theme: "dark" }); await flush(); - // Send another partial update: only viewport and containerDimensions change + // Send another partial update: only containerDimensions change newBridge.sendHostContextChange({ - viewport: { width: 1024, height: 768 }, containerDimensions: { width: 1024, maxHeight: 768 }, }); await flush(); @@ -366,14 +363,10 @@ describe("App <-> AppBridge integration", () => { // getHostContext should have accumulated all updates: // - locale from initial (unchanged) // - theme from first partial update - // - viewport and containerDimensions from second partial update + // - containerDimensions from second partial update const context = newApp.getHostContext(); expect(context?.theme).toBe("dark"); expect(context?.locale).toBe("en-US"); - expect(context?.viewport).toEqual({ - width: 1024, - height: 768, - }); expect(context?.containerDimensions).toEqual({ width: 1024, maxHeight: 768, diff --git a/src/app-bridge.ts b/src/app-bridge.ts index 8a06f9e..f9ff699 100644 --- a/src/app-bridge.ts +++ b/src/app-bridge.ts @@ -356,7 +356,7 @@ export class AppBridge extends Protocol< * adjust the iframe container dimensions based on the Guest UI's content. * * Note: This is for Guest UI → Host communication. To notify the Guest UI of - * host viewport changes, use {@link app.App.sendSizeChanged}. + * host container dimension changes, use {@link setHostContext}. * * @example * ```typescript @@ -1008,7 +1008,6 @@ export class AppBridge extends Protocol< * ```typescript * bridge.setHostContext({ * theme: "dark", - * viewport: { width: 800, height: 600 }, * containerDimensions: { maxHeight: 600, width: 800 } * }); * ``` diff --git a/src/app.ts b/src/app.ts index 3188530..ad97fd3 100644 --- a/src/app.ts +++ b/src/app.ts @@ -154,7 +154,7 @@ type RequestHandlerExtra = Parameters< * - `ontoolinput` - Complete tool arguments from host * - `ontoolinputpartial` - Streaming partial tool arguments * - `ontoolresult` - Tool execution results - * - `onhostcontextchanged` - Host context changes (theme, viewport, etc.) + * - `onhostcontextchanged` - Host context changes (theme, locale, etc.) * * These setters are convenience wrappers around `setNotificationHandler()`. * Both patterns work; use whichever fits your coding style better. @@ -293,7 +293,7 @@ export class App extends Protocol { * Get the host context discovered during initialization. * * Returns the host context that was provided in the initialization response, - * including tool info, theme, viewport, locale, and other environment details. + * including tool info, theme, locale, and other environment details. * This context is automatically updated when the host sends * `ui/notifications/host-context-changed` notifications. * @@ -478,12 +478,12 @@ export class App extends Protocol { } /** - * Convenience handler for host context changes (theme, viewport, locale, etc.). + * Convenience handler for host context changes (theme, locale, etc.). * * Set this property to register a handler that will be called when the host's - * context changes, such as theme switching (light/dark), viewport size changes, - * locale changes, or other environmental updates. Apps should respond by - * updating their UI accordingly. + * context changes, such as theme switching (light/dark), locale changes, or + * other environmental updates. Apps should respond by updating their UI + * accordingly. * * This setter is a convenience wrapper around `setNotificationHandler()` that * automatically handles the notification schema and extracts the params for you. diff --git a/src/generated/schema.json b/src/generated/schema.json index a5eb424..995eb4d 100644 --- a/src/generated/schema.json +++ b/src/generated/schema.json @@ -697,31 +697,6 @@ } ] }, - "viewport": { - "description": "Window viewport dimensions. Represents the host window's viewport size,\nwhich provides additional information apps can use to make responsive layout decisions.", - "type": "object", - "properties": { - "width": { - "description": "Window viewport width in pixels.", - "anyOf": [ - { - "type": "number" - }, - {} - ] - }, - "height": { - "description": "Window viewport height in pixels.", - "anyOf": [ - { - "type": "number" - }, - {} - ] - } - }, - "additionalProperties": false - }, "locale": { "description": "User's language and region preference in BCP 47 format.", "type": "string" @@ -1393,31 +1368,6 @@ } ] }, - "viewport": { - "description": "Window viewport dimensions. Represents the host window's viewport size,\nwhich provides additional information apps can use to make responsive layout decisions.", - "type": "object", - "properties": { - "width": { - "description": "Window viewport width in pixels.", - "anyOf": [ - { - "type": "number" - }, - {} - ] - }, - "height": { - "description": "Window viewport height in pixels.", - "anyOf": [ - { - "type": "number" - }, - {} - ] - } - }, - "additionalProperties": false - }, "locale": { "description": "User's language and region preference in BCP 47 format.", "type": "string" @@ -2626,31 +2576,6 @@ } ] }, - "viewport": { - "description": "Window viewport dimensions. Represents the host window's viewport size,\nwhich provides additional information apps can use to make responsive layout decisions.", - "type": "object", - "properties": { - "width": { - "description": "Window viewport width in pixels.", - "anyOf": [ - { - "type": "number" - }, - {} - ] - }, - "height": { - "description": "Window viewport height in pixels.", - "anyOf": [ - { - "type": "number" - }, - {} - ] - } - }, - "additionalProperties": false - }, "locale": { "description": "User's language and region preference in BCP 47 format.", "type": "string" diff --git a/src/generated/schema.ts b/src/generated/schema.ts index dfaafa5..bc415e5 100644 --- a/src/generated/schema.ts +++ b/src/generated/schema.ts @@ -601,27 +601,6 @@ export const McpUiHostContextSchema = z .describe( "Container dimensions. Represents the dimensions of the iframe or other\ncontainer holding the app. Specify either width or maxWidth, and either height or maxHeight.", ), - /** - * @description Window viewport dimensions. Represents the host window's viewport size, - * which provides additional information apps can use to make responsive layout decisions. - */ - viewport: z - .object({ - /** @description Window viewport width in pixels. */ - width: z - .union([z.number(), z.undefined()]) - .optional() - .describe("Window viewport width in pixels."), - /** @description Window viewport height in pixels. */ - height: z - .union([z.number(), z.undefined()]) - .optional() - .describe("Window viewport height in pixels."), - }) - .optional() - .describe( - "Window viewport dimensions. Represents the host window's viewport size,\nwhich provides additional information apps can use to make responsive layout decisions.", - ), /** @description User's language and region preference in BCP 47 format. */ locale: z .string() diff --git a/src/spec.types.ts b/src/spec.types.ts index 57627e7..5b7135b 100644 --- a/src/spec.types.ts +++ b/src/spec.types.ts @@ -349,16 +349,6 @@ export interface McpUiHostContext { maxWidth?: number | undefined; } ); - /** - * @description Window viewport dimensions. Represents the host window's viewport size, - * which provides additional information apps can use to make responsive layout decisions. - */ - viewport?: { - /** @description Window viewport width in pixels. */ - width?: number | undefined; - /** @description Window viewport height in pixels. */ - height?: number | undefined; - }; /** @description User's language and region preference in BCP 47 format. */ locale?: string; /** @description User's timezone in IANA format. */