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 6e33f813c..e5ff305dd 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 969030fba..3424d69c2 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 2c63eaa97..111be345c 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 e83598652..f1a458ccd 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" - }, - "height": { - "description": "Current viewport height in pixels.", - "type": "number" - }, - "maxHeight": { - "description": "Maximum available height in pixels (if constrained).", - "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 + } + ] }, - "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" - }, - "height": { - "description": "Current viewport height in pixels.", - "type": "number" - }, - "maxHeight": { - "description": "Maximum available height in pixels (if constrained).", - "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 + } + ] }, - "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" - }, - "height": { - "description": "Current viewport height in pixels.", - "type": "number" - }, - "maxHeight": { - "description": "Maximum available height in pixels (if constrained).", - "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 + } + ] }, - "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 9e6120a56..555896171 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.", - ), - /** @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.", +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 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 f97a0a6f8..1a0c8c2c5 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 e5ff305dd..f4aa9d5ae 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 3424d69c2..5a36b4779 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 f1a458ccd..e83598652 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 555896171..5e59edc66 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 1a0c8c2c5..f97a0a6f8 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 f4aa9d5ae..c7d983873 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 e83598652..839e17430 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 5e59edc66..f6deae591 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 f97a0a6f8..13ee62525 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 | 36 ++++++++++++++++++------------------ src/generated/schema.ts | 28 ++++++++++++++++++---------- src/spec.types.ts | 10 +++++----- 4 files changed, 46 insertions(+), 38 deletions(-) diff --git a/specification/draft/apps.mdx b/specification/draft/apps.mdx index c7d983873..facb348d3 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 839e17430..6895edcee 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.", + "description": "Viewport height in pixels (if fixed). Only pass height or maxHeight, not both.", "type": "number" }, - "maxHeight": { - "description": "Maximum available height in pixels (if constrained).", + "maxWidth": { + "description": "Maximum available viewport width in pixels (if constrained). Only pass width or maxWidth, not both.", "type": "number" }, - "maxWidth": { - "description": "Maximum available width in pixels (if constrained).", + "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.", + "description": "Viewport height in pixels (if fixed). Only pass height or maxHeight, not both.", "type": "number" }, - "maxHeight": { - "description": "Maximum available height in pixels (if constrained).", + "maxWidth": { + "description": "Maximum available viewport width in pixels (if constrained). Only pass width or maxWidth, not both.", "type": "number" }, - "maxWidth": { - "description": "Maximum available width in pixels (if constrained).", + "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.", + "description": "Viewport height in pixels (if fixed). Only pass height or maxHeight, not both.", "type": "number" }, - "maxHeight": { - "description": "Maximum available height in pixels (if constrained).", + "maxWidth": { + "description": "Maximum available viewport width in pixels (if constrained). Only pass width or maxWidth, not both.", "type": "number" }, - "maxWidth": { - "description": "Maximum available width in pixels (if constrained).", + "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 f6deae591..2c73ad673 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 + .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 height in pixels (if constrained)."), - /** @description Maximum available width in pixels (if constrained). */ - maxWidth: z + .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 width in pixels (if constrained)."), + .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 13ee62525..8f04e7ebc 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 6895edcee..fed9bb75c 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 2c73ad673..16c0895e9 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 8f04e7ebc..1d038198f 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 | 243 +++++++++++++++++++++++++++-------- src/generated/schema.ts | 72 +++++++---- src/spec.types.ts | 37 +++++- 6 files changed, 320 insertions(+), 110 deletions(-) diff --git a/specification/draft/apps.mdx b/specification/draft/apps.mdx index facb348d3..51898a122 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 5a36b4779..ce3a3388d 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 111be345c..8a06f9e9f 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 fed9bb75c..a5eb424ae 100644 --- a/src/generated/schema.json +++ b/src/generated/schema.json @@ -632,30 +632,77 @@ "type": "string" } }, - "viewport": { - "description": "Current and maximum dimensions available to the UI.", - "type": "object", - "properties": { - "width": { - "description": "Viewport width in pixels (if fixed). Only pass width or maxWidth, not both.", + "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": "number" + "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 + } ] }, - "height": { - "description": "Viewport height in pixels (if fixed). Only pass height or maxHeight, not both.", + { "anyOf": [ { - "type": "number" + "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 + } ] - }, - "maxWidth": { - "description": "Maximum available viewport width in pixels (if constrained). Only pass width or maxWidth, not both.", + } + ] + }, + "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" @@ -663,8 +710,8 @@ {} ] }, - "maxHeight": { - "description": "Maximum available viewport height in pixels (if constrained). Only pass height or maxHeight, not both.", + "height": { + "description": "Window viewport height in pixels.", "anyOf": [ { "type": "number" @@ -1281,30 +1328,77 @@ "type": "string" } }, - "viewport": { - "description": "Current and maximum dimensions available to the UI.", - "type": "object", - "properties": { - "width": { - "description": "Viewport width in pixels (if fixed). Only pass width or maxWidth, not both.", + "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": "number" + "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 + } ] }, - "height": { - "description": "Viewport height in pixels (if fixed). Only pass height or maxHeight, not both.", + { "anyOf": [ { - "type": "number" + "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 + } ] - }, - "maxWidth": { - "description": "Maximum available viewport width in pixels (if constrained). Only pass width or maxWidth, not both.", + } + ] + }, + "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" @@ -1312,8 +1406,8 @@ {} ] }, - "maxHeight": { - "description": "Maximum available viewport height in pixels (if constrained). Only pass height or maxHeight, not both.", + "height": { + "description": "Window viewport height in pixels.", "anyOf": [ { "type": "number" @@ -2467,30 +2561,77 @@ "type": "string" } }, - "viewport": { - "description": "Current and maximum dimensions available to the UI.", - "type": "object", - "properties": { - "width": { - "description": "Viewport width in pixels (if fixed). Only pass width or maxWidth, not both.", + "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": "number" + "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 + } ] }, - "height": { - "description": "Viewport height in pixels (if fixed). Only pass height or maxHeight, not both.", + { "anyOf": [ { - "type": "number" + "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 + } ] - }, - "maxWidth": { - "description": "Maximum available viewport width in pixels (if constrained). Only pass width or maxWidth, not both.", + } + ] + }, + "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" @@ -2498,8 +2639,8 @@ {} ] }, - "maxHeight": { - "description": "Maximum available viewport height in pixels (if constrained). Only pass height or maxHeight, not both.", + "height": { + "description": "Window viewport height in pixels.", "anyOf": [ { "type": "number" diff --git a/src/generated/schema.ts b/src/generated/schema.ts index 16c0895e9..3abd30c54 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 1d038198f..57627e78b 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 3abd30c54..dfaafa519 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 8d46e185b..42df7527a 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 51898a122..830500338 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 ce3a3388d..1e55f6bd5 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 8a06f9e9f..f9ff6999f 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 318853027..ad97fd331 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 a5eb424ae..995eb4dd1 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 dfaafa519..bc415e51e 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 57627e78b..5b7135ba2 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. */