From 9e97bf3dfcc282367d1195cb836484922769dc01 Mon Sep 17 00:00:00 2001 From: martinalong Date: Wed, 10 Dec 2025 15:21:55 -0800 Subject: [PATCH 01/15] Add styles prop to host context --- src/spec.types.ts | 60 +++++++++++++++++++++++++++++++++++++++++++++++ src/types.ts | 2 ++ 2 files changed, 62 insertions(+) diff --git a/src/spec.types.ts b/src/spec.types.ts index 01cc47136..0dbfb0355 100644 --- a/src/spec.types.ts +++ b/src/spec.types.ts @@ -36,6 +36,64 @@ export type McpUiTheme = "light" | "dark"; */ export type McpUiDisplayMode = "inline" | "fullscreen" | "pip"; +/** + * @description CSS variable keys available to MCP apps for theming. + */ +export type McpUiStyleVariableKey = + // Background colors + | "--color-background-primary" + | "--color-background-secondary" + | "--color-background-tertiary" + | "--color-background-inverted" + // Text colors + | "--color-text-primary" + | "--color-text-secondary" + | "--color-text-tertiary" + | "--color-text-inverted" + // Icon colors + | "--color-icon-primary" + | "--color-icon-secondary" + | "--color-icon-tertiary" + | "--color-icon-inverted" + // Border colors + | "--color-border-primary" + | "--color-border-secondary" + // Accent colors + | "--color-accent-info" + | "--color-accent-danger" + | "--color-accent-success" + | "--color-accent-warning" + // Typography - Family + | "--font-family-sans" + // Typography - Size + | "--font-size-heading" + | "--font-size-body" + | "--font-size-caption" + // Typography - Weight + | "--font-weight-regular" + | "--font-weight-emphasized" + // Typography - Line height + | "--font-leading-regular" + | "--font-leading-tight" + // Typography - Composite styles + | "--font-style-heading" + | "--font-style-body" + | "--font-style-body-emphasized" + | "--font-style-caption" + | "--font-style-caption-emphasized" + // Border radius + | "--border-radius-small" + | "--border-radius-medium" + | "--border-radius-large" + | "--border-radius-full" + // Border width + | "--border-width-regular"; + +/** + * @description Style variables for theming MCP apps. + */ +export type McpUiStyles = Record; + /** * @description Request to open an external URL in the host's default browser. * @see {@link app.App.sendOpenLink} for the method that sends this request @@ -195,6 +253,8 @@ export interface McpUiHostContext { }; /** @description Current color theme preference. */ theme?: McpUiTheme; + /** @description CSS variables for theming the app. */ + styles?: McpUiStyles; /** @description How the UI is currently displayed. */ displayMode?: McpUiDisplayMode; /** @description Display modes the host supports. */ diff --git a/src/types.ts b/src/types.ts index 8544b27e8..fa8708593 100644 --- a/src/types.ts +++ b/src/types.ts @@ -14,6 +14,8 @@ export { LATEST_PROTOCOL_VERSION, type McpUiTheme, type McpUiDisplayMode, + type McpUiStyleVariableKey, + type McpUiStyles, type McpUiOpenLinkRequest, type McpUiOpenLinkResult, type McpUiMessageRequest, From aa8968808a3e69b908cb1a63ef4f6ae265179116 Mon Sep 17 00:00:00 2001 From: martinalong Date: Wed, 10 Dec 2025 15:39:22 -0800 Subject: [PATCH 02/15] Add utility for using styles --- src/app.ts | 1 + src/react/index.tsx | 2 + src/react/useHostStyles.ts | 84 ++++++++++++++++++++++++++++++++++++++ src/styles.ts | 41 +++++++++++++++++++ 4 files changed, 128 insertions(+) create mode 100644 src/react/useHostStyles.ts create mode 100644 src/styles.ts diff --git a/src/app.ts b/src/app.ts index c4970fcd6..23213745c 100644 --- a/src/app.ts +++ b/src/app.ts @@ -47,6 +47,7 @@ import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js"; export { PostMessageTransport } from "./message-transport"; export * from "./types"; +export { applyHostStyles } from "./styles"; /** * Metadata key for associating a resource URI with a tool call. diff --git a/src/react/index.tsx b/src/react/index.tsx index d5ce3760b..82867df73 100644 --- a/src/react/index.tsx +++ b/src/react/index.tsx @@ -9,6 +9,7 @@ * ## Main Exports * * - {@link useApp} - React hook to create and connect an MCP App + * - {@link useHostStyles} - React hook to apply host styles as CSS variables * - {@link useAutoResize} - React hook for manual auto-resize control (rarely needed) * * @module @modelcontextprotocol/ext-apps/react @@ -32,3 +33,4 @@ */ export * from "./useApp"; export * from "./useAutoResize"; +export * from "./useHostStyles"; diff --git a/src/react/useHostStyles.ts b/src/react/useHostStyles.ts new file mode 100644 index 000000000..9fa7ad578 --- /dev/null +++ b/src/react/useHostStyles.ts @@ -0,0 +1,84 @@ +import { useEffect, useRef } from "react"; +import { App } from "../app"; +import { applyHostStyles } from "../styles"; +import { McpUiHostContext } from "../types"; + +/** + * React hook that applies host styles as CSS custom properties. + * + * This hook listens to host context changes and automatically applies the + * `styles` CSS variables to `document.documentElement`. This allows your + * app to use the host's theming values via CSS variables like + * `var(--color-background-primary)`. + * + * The hook also applies styles from the initial host context when the app + * first connects. + * + * @param app - The connected App instance, or null during initialization + * @param initialContext - Initial host context from the connection (optional). + * If provided, styles will be applied immediately on mount. + * + * @example Basic usage with useApp + * ```tsx + * import { useApp } from '@modelcontextprotocol/ext-apps/react'; + * import { useHostStyles } from '@modelcontextprotocol/ext-apps/react'; + * + * function MyApp() { + * const { app, isConnected } = useApp({ + * appInfo: { name: "MyApp", version: "1.0.0" }, + * capabilities: {}, + * }); + * + * // Automatically apply host styles as CSS variables + * useHostStyles(app); + * + * return ( + *
+ * Hello! + *
+ * ); + * } + * ``` + * + * @example With initial context + * ```tsx + * const [hostContext, setHostContext] = useState(null); + * + * // ... get initial context from app.connect() result + * + * useHostStyles(app, hostContext); + * ``` + * + * @see {@link applyHostStyles} for the underlying function + * @see {@link McpUiStyles} for available CSS variables + */ +export function useHostStyles( + app: App | null, + initialContext?: McpUiHostContext | null, +): void { + const initialStylesApplied = useRef(false); + + // Apply initial styles once on mount + useEffect(() => { + if (initialStylesApplied.current) { + return; + } + if (initialContext?.styles) { + applyHostStyles(initialContext.styles); + initialStylesApplied.current = true; + } + }, [initialContext]); + + // Listen for host context changes and apply updated styles + useEffect(() => { + if (!app) { + return; + } + + app.onhostcontextchanged = (params) => { + if (params.styles) { + applyHostStyles(params.styles); + } + }; + }, [app]); +} diff --git a/src/styles.ts b/src/styles.ts new file mode 100644 index 000000000..52ab6a135 --- /dev/null +++ b/src/styles.ts @@ -0,0 +1,41 @@ +import { McpUiStyles } from "./types"; + +/** + * Apply host styles as CSS custom properties on an element. + * + * This function takes the `styles` object from `McpUiHostContext` and sets + * each CSS variable on the specified root element (defaults to `document.documentElement`). + * This allows apps to use the host's theming values via CSS variables like + * `var(--color-background-primary)`. + * + * @param styles - The styles object from `McpUiHostContext.styles` + * @param root - The element to apply styles to (defaults to `document.documentElement`) + * + * @example Apply styles from host context + * ```typescript + * import { applyHostStyles } from '@modelcontextprotocol/ext-apps'; + * + * app.onhostcontextchanged = (params) => { + * if (params.styles) { + * applyHostStyles(params.styles); + * } + * }; + * ``` + * + * @example Apply to a specific element + * ```typescript + * const container = document.getElementById('app-root'); + * applyHostStyles(hostContext.styles, container); + * ``` + * + * @see {@link McpUiStyles} for the available CSS variables + * @see {@link McpUiHostContext} for the full host context structure + */ +export function applyHostStyles( + styles: McpUiStyles, + root: HTMLElement = document.documentElement, +): void { + for (const [key, value] of Object.entries(styles)) { + root.style.setProperty(key, value); + } +} From a8b3bf0ccd464b1835d76f6b109c5845bd49a084 Mon Sep 17 00:00:00 2001 From: martinalong Date: Wed, 10 Dec 2025 15:44:48 -0800 Subject: [PATCH 03/15] Add utility to set + read theme --- src/app.ts | 6 ++- src/react/index.tsx | 2 + src/react/useDocumentTheme.ts | 69 ++++++++++++++++++++++++++++++++ src/styles.ts | 75 ++++++++++++++++++++++++++++++++++- 4 files changed, 150 insertions(+), 2 deletions(-) create mode 100644 src/react/useDocumentTheme.ts diff --git a/src/app.ts b/src/app.ts index 23213745c..b5233dff0 100644 --- a/src/app.ts +++ b/src/app.ts @@ -47,7 +47,11 @@ import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js"; export { PostMessageTransport } from "./message-transport"; export * from "./types"; -export { applyHostStyles } from "./styles"; +export { + applyHostStyles, + getDocumentTheme, + applyDocumentTheme, +} from "./styles"; /** * Metadata key for associating a resource URI with a tool call. diff --git a/src/react/index.tsx b/src/react/index.tsx index 82867df73..09e3b8721 100644 --- a/src/react/index.tsx +++ b/src/react/index.tsx @@ -10,6 +10,7 @@ * * - {@link useApp} - React hook to create and connect an MCP App * - {@link useHostStyles} - React hook to apply host styles as CSS variables + * - {@link useDocumentTheme} - React hook for reactive document theme * - {@link useAutoResize} - React hook for manual auto-resize control (rarely needed) * * @module @modelcontextprotocol/ext-apps/react @@ -33,4 +34,5 @@ */ export * from "./useApp"; export * from "./useAutoResize"; +export * from "./useDocumentTheme"; export * from "./useHostStyles"; diff --git a/src/react/useDocumentTheme.ts b/src/react/useDocumentTheme.ts new file mode 100644 index 000000000..2355c68fb --- /dev/null +++ b/src/react/useDocumentTheme.ts @@ -0,0 +1,69 @@ +import { useEffect, useState } from "react"; +import { getDocumentTheme } from "../styles"; +import { McpUiTheme } from "../types"; + +/** + * React hook that provides the current document theme reactively. + * + * Uses a MutationObserver to watch for changes to the `data-theme` attribute + * or `class` on `document.documentElement`. When the theme changes (e.g., from + * host context updates), the hook automatically re-renders your component with + * the new theme value. + * + * @returns The current theme ("light" or "dark") + * + * @example Conditionally render based on theme + * ```tsx + * import { useDocumentTheme } from '@modelcontextprotocol/ext-apps/react'; + * + * function MyApp() { + * const theme = useDocumentTheme(); + * + * return ( + *
+ * {theme === 'dark' ? : } + *
+ * ); + * } + * ``` + * + * @example Use with theme-aware styling + * ```tsx + * function ThemedButton() { + * const theme = useDocumentTheme(); + * + * return ( + * + * ); + * } + * ``` + * + * @see {@link getDocumentTheme} for the underlying function + * @see {@link applyDocumentTheme} to set the theme + */ +export function useDocumentTheme(): McpUiTheme { + const [theme, setTheme] = useState(getDocumentTheme); + + useEffect(() => { + const observer = new MutationObserver(() => { + setTheme(getDocumentTheme()); + }); + + observer.observe(document.documentElement, { + attributes: true, + attributeFilter: ["data-theme", "class"], + characterData: false, + childList: false, + subtree: false, + }); + + return () => observer.disconnect(); + }, []); + + return theme; +} diff --git a/src/styles.ts b/src/styles.ts index 52ab6a135..454424839 100644 --- a/src/styles.ts +++ b/src/styles.ts @@ -1,4 +1,77 @@ -import { McpUiStyles } from "./types"; +import { McpUiStyles, McpUiTheme } from "./types"; + +/** + * Get the current document theme from the root HTML element. + * + * Reads the theme from the `data-theme` attribute on `document.documentElement`. + * Falls back to checking for a `dark` class for compatibility with Tailwind CSS + * dark mode conventions. + * + * @returns The current theme ("light" or "dark") + * + * @example Check current theme + * ```typescript + * import { getDocumentTheme } from '@modelcontextprotocol/ext-apps'; + * + * const theme = getDocumentTheme(); + * console.log(`Current theme: ${theme}`); + * ``` + * + * @see {@link applyDocumentTheme} to set the theme + * @see {@link McpUiTheme} for the theme type + */ +export function getDocumentTheme(): McpUiTheme { + const theme = document.documentElement.getAttribute("data-theme"); + + if (theme === "dark" || theme === "light") { + return theme; + } + + // Fallback: check for "dark" class (Tailwind CSS convention) + const darkMode = document.documentElement.classList.contains("dark"); + + return darkMode ? "dark" : "light"; +} + +/** + * Apply a theme to the document root element. + * + * Sets the `data-theme` attribute and CSS `color-scheme` property on + * `document.documentElement`. This enables CSS selectors like + * `[data-theme="dark"]` and ensures native elements (scrollbars, form controls) + * respect the theme. + * + * @param theme - The theme to apply ("light" or "dark") + * + * @example Apply theme from host context + * ```typescript + * import { applyDocumentTheme } from '@modelcontextprotocol/ext-apps'; + * + * app.onhostcontextchanged = (params) => { + * if (params.theme) { + * applyDocumentTheme(params.theme); + * } + * }; + * ``` + * + * @example Use with CSS selectors + * ```css + * [data-theme="dark"] { + * --bg-color: #1a1a1a; + * } + * [data-theme="light"] { + * --bg-color: #ffffff; + * } + * ``` + * + * @see {@link getDocumentTheme} to read the current theme + * @see {@link McpUiTheme} for the theme type + */ +export function applyDocumentTheme(theme: McpUiTheme): void { + const root = document.documentElement; + root.setAttribute("data-theme", theme); + root.style.colorScheme = theme; +} /** * Apply host styles as CSS custom properties on an element. From f276ec6df9a55afd1d62f38ff58cb50996a12921 Mon Sep 17 00:00:00 2001 From: martinalong Date: Wed, 10 Dec 2025 23:50:38 -0800 Subject: [PATCH 04/15] Regenerate schemas after npm install MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/generated/schema.json | 779 +++++++++++++++++++++++++++++++++++ src/generated/schema.test.ts | 16 + src/generated/schema.ts | 60 +++ src/react/useHostStyles.ts | 44 +- 4 files changed, 883 insertions(+), 16 deletions(-) diff --git a/src/generated/schema.json b/src/generated/schema.json index f70b60f99..63cb3d3b9 100644 --- a/src/generated/schema.json +++ b/src/generated/schema.json @@ -264,6 +264,163 @@ } ] }, + "styles": { + "description": "CSS variables for theming the app.", + "type": "object", + "propertyNames": { + "description": "Style variables for theming MCP apps.", + "anyOf": [ + { + "type": "string", + "const": "--color-background-primary" + }, + { + "type": "string", + "const": "--color-background-secondary" + }, + { + "type": "string", + "const": "--color-background-tertiary" + }, + { + "type": "string", + "const": "--color-background-inverted" + }, + { + "type": "string", + "const": "--color-text-primary" + }, + { + "type": "string", + "const": "--color-text-secondary" + }, + { + "type": "string", + "const": "--color-text-tertiary" + }, + { + "type": "string", + "const": "--color-text-inverted" + }, + { + "type": "string", + "const": "--color-icon-primary" + }, + { + "type": "string", + "const": "--color-icon-secondary" + }, + { + "type": "string", + "const": "--color-icon-tertiary" + }, + { + "type": "string", + "const": "--color-icon-inverted" + }, + { + "type": "string", + "const": "--color-border-primary" + }, + { + "type": "string", + "const": "--color-border-secondary" + }, + { + "type": "string", + "const": "--color-accent-info" + }, + { + "type": "string", + "const": "--color-accent-danger" + }, + { + "type": "string", + "const": "--color-accent-success" + }, + { + "type": "string", + "const": "--color-accent-warning" + }, + { + "type": "string", + "const": "--font-family-sans" + }, + { + "type": "string", + "const": "--font-size-heading" + }, + { + "type": "string", + "const": "--font-size-body" + }, + { + "type": "string", + "const": "--font-size-caption" + }, + { + "type": "string", + "const": "--font-weight-regular" + }, + { + "type": "string", + "const": "--font-weight-emphasized" + }, + { + "type": "string", + "const": "--font-leading-regular" + }, + { + "type": "string", + "const": "--font-leading-tight" + }, + { + "type": "string", + "const": "--font-style-heading" + }, + { + "type": "string", + "const": "--font-style-body" + }, + { + "type": "string", + "const": "--font-style-body-emphasized" + }, + { + "type": "string", + "const": "--font-style-caption" + }, + { + "type": "string", + "const": "--font-style-caption-emphasized" + }, + { + "type": "string", + "const": "--border-radius-small" + }, + { + "type": "string", + "const": "--border-radius-medium" + }, + { + "type": "string", + "const": "--border-radius-large" + }, + { + "type": "string", + "const": "--border-radius-full" + }, + { + "type": "string", + "const": "--border-width-regular" + } + ] + }, + "additionalProperties": { + "description": "Style variables for theming MCP apps.", + "type": "string" + } + }, "displayMode": { "description": "How the UI is currently displayed.", "anyOf": [ @@ -550,6 +707,163 @@ } ] }, + "styles": { + "description": "CSS variables for theming the app.", + "type": "object", + "propertyNames": { + "description": "Style variables for theming MCP apps.", + "anyOf": [ + { + "type": "string", + "const": "--color-background-primary" + }, + { + "type": "string", + "const": "--color-background-secondary" + }, + { + "type": "string", + "const": "--color-background-tertiary" + }, + { + "type": "string", + "const": "--color-background-inverted" + }, + { + "type": "string", + "const": "--color-text-primary" + }, + { + "type": "string", + "const": "--color-text-secondary" + }, + { + "type": "string", + "const": "--color-text-tertiary" + }, + { + "type": "string", + "const": "--color-text-inverted" + }, + { + "type": "string", + "const": "--color-icon-primary" + }, + { + "type": "string", + "const": "--color-icon-secondary" + }, + { + "type": "string", + "const": "--color-icon-tertiary" + }, + { + "type": "string", + "const": "--color-icon-inverted" + }, + { + "type": "string", + "const": "--color-border-primary" + }, + { + "type": "string", + "const": "--color-border-secondary" + }, + { + "type": "string", + "const": "--color-accent-info" + }, + { + "type": "string", + "const": "--color-accent-danger" + }, + { + "type": "string", + "const": "--color-accent-success" + }, + { + "type": "string", + "const": "--color-accent-warning" + }, + { + "type": "string", + "const": "--font-family-sans" + }, + { + "type": "string", + "const": "--font-size-heading" + }, + { + "type": "string", + "const": "--font-size-body" + }, + { + "type": "string", + "const": "--font-size-caption" + }, + { + "type": "string", + "const": "--font-weight-regular" + }, + { + "type": "string", + "const": "--font-weight-emphasized" + }, + { + "type": "string", + "const": "--font-leading-regular" + }, + { + "type": "string", + "const": "--font-leading-tight" + }, + { + "type": "string", + "const": "--font-style-heading" + }, + { + "type": "string", + "const": "--font-style-body" + }, + { + "type": "string", + "const": "--font-style-body-emphasized" + }, + { + "type": "string", + "const": "--font-style-caption" + }, + { + "type": "string", + "const": "--font-style-caption-emphasized" + }, + { + "type": "string", + "const": "--border-radius-small" + }, + { + "type": "string", + "const": "--border-radius-medium" + }, + { + "type": "string", + "const": "--border-radius-large" + }, + { + "type": "string", + "const": "--border-radius-full" + }, + { + "type": "string", + "const": "--border-width-regular" + } + ] + }, + "additionalProperties": { + "description": "Style variables for theming MCP apps.", + "type": "string" + } + }, "displayMode": { "description": "How the UI is currently displayed.", "anyOf": [ @@ -1018,6 +1332,163 @@ } ] }, + "styles": { + "description": "CSS variables for theming the app.", + "type": "object", + "propertyNames": { + "description": "Style variables for theming MCP apps.", + "anyOf": [ + { + "type": "string", + "const": "--color-background-primary" + }, + { + "type": "string", + "const": "--color-background-secondary" + }, + { + "type": "string", + "const": "--color-background-tertiary" + }, + { + "type": "string", + "const": "--color-background-inverted" + }, + { + "type": "string", + "const": "--color-text-primary" + }, + { + "type": "string", + "const": "--color-text-secondary" + }, + { + "type": "string", + "const": "--color-text-tertiary" + }, + { + "type": "string", + "const": "--color-text-inverted" + }, + { + "type": "string", + "const": "--color-icon-primary" + }, + { + "type": "string", + "const": "--color-icon-secondary" + }, + { + "type": "string", + "const": "--color-icon-tertiary" + }, + { + "type": "string", + "const": "--color-icon-inverted" + }, + { + "type": "string", + "const": "--color-border-primary" + }, + { + "type": "string", + "const": "--color-border-secondary" + }, + { + "type": "string", + "const": "--color-accent-info" + }, + { + "type": "string", + "const": "--color-accent-danger" + }, + { + "type": "string", + "const": "--color-accent-success" + }, + { + "type": "string", + "const": "--color-accent-warning" + }, + { + "type": "string", + "const": "--font-family-sans" + }, + { + "type": "string", + "const": "--font-size-heading" + }, + { + "type": "string", + "const": "--font-size-body" + }, + { + "type": "string", + "const": "--font-size-caption" + }, + { + "type": "string", + "const": "--font-weight-regular" + }, + { + "type": "string", + "const": "--font-weight-emphasized" + }, + { + "type": "string", + "const": "--font-leading-regular" + }, + { + "type": "string", + "const": "--font-leading-tight" + }, + { + "type": "string", + "const": "--font-style-heading" + }, + { + "type": "string", + "const": "--font-style-body" + }, + { + "type": "string", + "const": "--font-style-body-emphasized" + }, + { + "type": "string", + "const": "--font-style-caption" + }, + { + "type": "string", + "const": "--font-style-caption-emphasized" + }, + { + "type": "string", + "const": "--border-radius-small" + }, + { + "type": "string", + "const": "--border-radius-medium" + }, + { + "type": "string", + "const": "--border-radius-large" + }, + { + "type": "string", + "const": "--border-radius-full" + }, + { + "type": "string", + "const": "--border-width-regular" + } + ] + }, + "additionalProperties": { + "description": "Style variables for theming MCP apps.", + "type": "string" + } + }, "displayMode": { "description": "How the UI is currently displayed.", "anyOf": [ @@ -1718,6 +2189,314 @@ "required": ["method", "params"], "additionalProperties": false }, + "McpUiStyleVariableKey": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "description": "CSS variable keys available to MCP apps for theming.", + "anyOf": [ + { + "type": "string", + "const": "--color-background-primary" + }, + { + "type": "string", + "const": "--color-background-secondary" + }, + { + "type": "string", + "const": "--color-background-tertiary" + }, + { + "type": "string", + "const": "--color-background-inverted" + }, + { + "type": "string", + "const": "--color-text-primary" + }, + { + "type": "string", + "const": "--color-text-secondary" + }, + { + "type": "string", + "const": "--color-text-tertiary" + }, + { + "type": "string", + "const": "--color-text-inverted" + }, + { + "type": "string", + "const": "--color-icon-primary" + }, + { + "type": "string", + "const": "--color-icon-secondary" + }, + { + "type": "string", + "const": "--color-icon-tertiary" + }, + { + "type": "string", + "const": "--color-icon-inverted" + }, + { + "type": "string", + "const": "--color-border-primary" + }, + { + "type": "string", + "const": "--color-border-secondary" + }, + { + "type": "string", + "const": "--color-accent-info" + }, + { + "type": "string", + "const": "--color-accent-danger" + }, + { + "type": "string", + "const": "--color-accent-success" + }, + { + "type": "string", + "const": "--color-accent-warning" + }, + { + "type": "string", + "const": "--font-family-sans" + }, + { + "type": "string", + "const": "--font-size-heading" + }, + { + "type": "string", + "const": "--font-size-body" + }, + { + "type": "string", + "const": "--font-size-caption" + }, + { + "type": "string", + "const": "--font-weight-regular" + }, + { + "type": "string", + "const": "--font-weight-emphasized" + }, + { + "type": "string", + "const": "--font-leading-regular" + }, + { + "type": "string", + "const": "--font-leading-tight" + }, + { + "type": "string", + "const": "--font-style-heading" + }, + { + "type": "string", + "const": "--font-style-body" + }, + { + "type": "string", + "const": "--font-style-body-emphasized" + }, + { + "type": "string", + "const": "--font-style-caption" + }, + { + "type": "string", + "const": "--font-style-caption-emphasized" + }, + { + "type": "string", + "const": "--border-radius-small" + }, + { + "type": "string", + "const": "--border-radius-medium" + }, + { + "type": "string", + "const": "--border-radius-large" + }, + { + "type": "string", + "const": "--border-radius-full" + }, + { + "type": "string", + "const": "--border-width-regular" + } + ] + }, + "McpUiStyles": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "description": "Style variables for theming MCP apps.", + "type": "object", + "propertyNames": { + "description": "Style variables for theming MCP apps.", + "anyOf": [ + { + "type": "string", + "const": "--color-background-primary" + }, + { + "type": "string", + "const": "--color-background-secondary" + }, + { + "type": "string", + "const": "--color-background-tertiary" + }, + { + "type": "string", + "const": "--color-background-inverted" + }, + { + "type": "string", + "const": "--color-text-primary" + }, + { + "type": "string", + "const": "--color-text-secondary" + }, + { + "type": "string", + "const": "--color-text-tertiary" + }, + { + "type": "string", + "const": "--color-text-inverted" + }, + { + "type": "string", + "const": "--color-icon-primary" + }, + { + "type": "string", + "const": "--color-icon-secondary" + }, + { + "type": "string", + "const": "--color-icon-tertiary" + }, + { + "type": "string", + "const": "--color-icon-inverted" + }, + { + "type": "string", + "const": "--color-border-primary" + }, + { + "type": "string", + "const": "--color-border-secondary" + }, + { + "type": "string", + "const": "--color-accent-info" + }, + { + "type": "string", + "const": "--color-accent-danger" + }, + { + "type": "string", + "const": "--color-accent-success" + }, + { + "type": "string", + "const": "--color-accent-warning" + }, + { + "type": "string", + "const": "--font-family-sans" + }, + { + "type": "string", + "const": "--font-size-heading" + }, + { + "type": "string", + "const": "--font-size-body" + }, + { + "type": "string", + "const": "--font-size-caption" + }, + { + "type": "string", + "const": "--font-weight-regular" + }, + { + "type": "string", + "const": "--font-weight-emphasized" + }, + { + "type": "string", + "const": "--font-leading-regular" + }, + { + "type": "string", + "const": "--font-leading-tight" + }, + { + "type": "string", + "const": "--font-style-heading" + }, + { + "type": "string", + "const": "--font-style-body" + }, + { + "type": "string", + "const": "--font-style-body-emphasized" + }, + { + "type": "string", + "const": "--font-style-caption" + }, + { + "type": "string", + "const": "--font-style-caption-emphasized" + }, + { + "type": "string", + "const": "--border-radius-small" + }, + { + "type": "string", + "const": "--border-radius-medium" + }, + { + "type": "string", + "const": "--border-radius-large" + }, + { + "type": "string", + "const": "--border-radius-full" + }, + { + "type": "string", + "const": "--border-width-regular" + } + ] + }, + "additionalProperties": { + "description": "Style variables for theming MCP apps.", + "type": "string" + } + }, "McpUiTheme": { "$schema": "https://json-schema.org/draft/2020-12/schema", "description": "Color theme preference for the host environment.", diff --git a/src/generated/schema.test.ts b/src/generated/schema.test.ts index 051ac8e55..3576dc244 100644 --- a/src/generated/schema.test.ts +++ b/src/generated/schema.test.ts @@ -19,6 +19,14 @@ export type McpUiDisplayModeSchemaInferredType = z.infer< typeof generated.McpUiDisplayModeSchema >; +export type McpUiStyleVariableKeySchemaInferredType = z.infer< + typeof generated.McpUiStyleVariableKeySchema +>; + +export type McpUiStylesSchemaInferredType = z.infer< + typeof generated.McpUiStylesSchema +>; + export type McpUiOpenLinkRequestSchemaInferredType = z.infer< typeof generated.McpUiOpenLinkRequestSchema >; @@ -111,6 +119,14 @@ expectType({} as McpUiThemeSchemaInferredType); expectType({} as spec.McpUiTheme); expectType({} as McpUiDisplayModeSchemaInferredType); expectType({} as spec.McpUiDisplayMode); +expectType( + {} as McpUiStyleVariableKeySchemaInferredType, +); +expectType( + {} as spec.McpUiStyleVariableKey, +); +expectType({} as McpUiStylesSchemaInferredType); +expectType({} as spec.McpUiStyles); expectType( {} as McpUiOpenLinkRequestSchemaInferredType, ); diff --git a/src/generated/schema.ts b/src/generated/schema.ts index baff96583..6b804dc44 100644 --- a/src/generated/schema.ts +++ b/src/generated/schema.ts @@ -24,6 +24,62 @@ export const McpUiDisplayModeSchema = z .union([z.literal("inline"), z.literal("fullscreen"), z.literal("pip")]) .describe("Display mode for UI presentation."); +/** + * @description CSS variable keys available to MCP apps for theming. + */ +export const McpUiStyleVariableKeySchema = z + .union([ + z.literal("--color-background-primary"), + z.literal("--color-background-secondary"), + z.literal("--color-background-tertiary"), + z.literal("--color-background-inverted"), + z.literal("--color-text-primary"), + z.literal("--color-text-secondary"), + z.literal("--color-text-tertiary"), + z.literal("--color-text-inverted"), + z.literal("--color-icon-primary"), + z.literal("--color-icon-secondary"), + z.literal("--color-icon-tertiary"), + z.literal("--color-icon-inverted"), + z.literal("--color-border-primary"), + z.literal("--color-border-secondary"), + z.literal("--color-accent-info"), + z.literal("--color-accent-danger"), + z.literal("--color-accent-success"), + z.literal("--color-accent-warning"), + z.literal("--font-family-sans"), + z.literal("--font-size-heading"), + z.literal("--font-size-body"), + z.literal("--font-size-caption"), + z.literal("--font-weight-regular"), + z.literal("--font-weight-emphasized"), + z.literal("--font-leading-regular"), + z.literal("--font-leading-tight"), + z.literal("--font-style-heading"), + z.literal("--font-style-body"), + z.literal("--font-style-body-emphasized"), + z.literal("--font-style-caption"), + z.literal("--font-style-caption-emphasized"), + z.literal("--border-radius-small"), + z.literal("--border-radius-medium"), + z.literal("--border-radius-large"), + z.literal("--border-radius-full"), + z.literal("--border-width-regular"), + ]) + .describe("CSS variable keys available to MCP apps for theming."); + +/** + * @description Style variables for theming MCP apps. + */ +export const McpUiStylesSchema = z + .record( + McpUiStyleVariableKeySchema.describe( + "Style variables for theming MCP apps.", + ), + z.string().describe("Style variables for theming MCP apps."), + ) + .describe("Style variables for theming MCP apps."); + /** * @description Request to open an external URL in the host's default browser. * @see {@link app.App.sendOpenLink} for the method that sends this request @@ -354,6 +410,10 @@ export const McpUiHostContextSchema = z.looseObject({ theme: McpUiThemeSchema.optional().describe( "Current color theme preference.", ), + /** @description CSS variables for theming the app. */ + styles: McpUiStylesSchema.optional().describe( + "CSS variables for theming the app.", + ), /** @description How the UI is currently displayed. */ displayMode: McpUiDisplayModeSchema.optional().describe( "How the UI is currently displayed.", diff --git a/src/react/useHostStyles.ts b/src/react/useHostStyles.ts index 9fa7ad578..0a2e32c22 100644 --- a/src/react/useHostStyles.ts +++ b/src/react/useHostStyles.ts @@ -1,22 +1,25 @@ import { useEffect, useRef } from "react"; import { App } from "../app"; -import { applyHostStyles } from "../styles"; +import { applyDocumentTheme, applyHostStyles } from "../styles"; import { McpUiHostContext } from "../types"; /** - * React hook that applies host styles as CSS custom properties. + * React hook that applies host styles and theme as CSS custom properties. * - * This hook listens to host context changes and automatically applies the - * `styles` CSS variables to `document.documentElement`. This allows your - * app to use the host's theming values via CSS variables like - * `var(--color-background-primary)`. + * This hook listens to host context changes and automatically applies: + * - `styles` CSS variables to `document.documentElement` (e.g., `--color-background-primary`) + * - `theme` via `color-scheme` CSS property, enabling `light-dark()` CSS function support * - * The hook also applies styles from the initial host context when the app - * first connects. + * The hook also applies styles and theme from the initial host context when + * the app first connects. + * + * **Note:** If the host provides style values using CSS `light-dark()` function, + * this hook ensures they work correctly by setting the `color-scheme` property + * based on the host's theme preference. * * @param app - The connected App instance, or null during initialization * @param initialContext - Initial host context from the connection (optional). - * If provided, styles will be applied immediately on mount. + * If provided, styles and theme will be applied immediately on mount. * * @example Basic usage with useApp * ```tsx @@ -29,7 +32,7 @@ import { McpUiHostContext } from "../types"; * capabilities: {}, * }); * - * // Automatically apply host styles as CSS variables + * // Automatically apply host styles and theme * useHostStyles(app); * * return ( @@ -49,33 +52,42 @@ import { McpUiHostContext } from "../types"; * useHostStyles(app, hostContext); * ``` * - * @see {@link applyHostStyles} for the underlying function + * @see {@link applyHostStyles} for the underlying styles function + * @see {@link applyDocumentTheme} for the underlying theme function * @see {@link McpUiStyles} for available CSS variables */ export function useHostStyles( app: App | null, initialContext?: McpUiHostContext | null, ): void { - const initialStylesApplied = useRef(false); + const initialApplied = useRef(false); - // Apply initial styles once on mount + // Apply initial styles and theme once on mount useEffect(() => { - if (initialStylesApplied.current) { + if (initialApplied.current) { return; } + if (initialContext?.theme) { + applyDocumentTheme(initialContext.theme); + } if (initialContext?.styles) { applyHostStyles(initialContext.styles); - initialStylesApplied.current = true; + } + if (initialContext?.theme || initialContext?.styles) { + initialApplied.current = true; } }, [initialContext]); - // Listen for host context changes and apply updated styles + // Listen for host context changes and apply updated styles/theme useEffect(() => { if (!app) { return; } app.onhostcontextchanged = (params) => { + if (params.theme) { + applyDocumentTheme(params.theme); + } if (params.styles) { applyHostStyles(params.styles); } From bcc05439c78983d6ad4b83ecd68d80946c04e743 Mon Sep 17 00:00:00 2001 From: Anton Pidkuiko MacBook Date: Thu, 11 Dec 2025 12:34:19 +0000 Subject: [PATCH 05/15] docs: add styles field to MCP Apps specification MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add styles field to HostContext interface with reference to Theming section - Add Theming section documenting 36 standardized CSS variables - Document Host/App behavior for theming including light-dark() usage - Add Design Decision #4 explaining CSS variables approach - Include JSON example showing light-dark() pattern 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- specification/draft/apps.mdx | 73 ++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/specification/draft/apps.mdx b/specification/draft/apps.mdx index f4a77c5ba..6f62ce688 100644 --- a/specification/draft/apps.mdx +++ b/specification/draft/apps.mdx @@ -404,6 +404,8 @@ interface HostContext { }; /** Current color theme preference */ theme?: "light" | "dark"; + /** CSS variables for theming. See Theming section for standardized variable names. */ + styles?: Record; /** How the UI is currently displayed */ displayMode?: "inline" | "fullscreen" | "pip"; /** Display modes the host supports */ @@ -440,6 +442,8 @@ interface HostContext { All fields are optional. Hosts SHOULD provide relevant context. Guest UIs SHOULD handle missing fields gracefully. +For `styles`, apps SHOULD provide CSS fallback values (e.g., `var(--color-text-primary, #171717)`) to handle hosts that don't supply styles. + Example: ```json @@ -453,6 +457,11 @@ Example: "hostInfo": { "name": "claude-desktop", "version": "1.0.0" }, "hostContext": { "theme": "dark", + "styles": { + "--color-background-primary": "light-dark(#ffffff, #171717)", + "--color-text-primary": "light-dark(#171717, #fafafa)", + "--font-family-sans": "system-ui, sans-serif" + }, "displayMode": "inline", "viewport": { "width": 400, "height": 300 } } @@ -460,6 +469,52 @@ Example: } ``` +### Theming + +Hosts pass CSS custom properties via `HostContext.styles` for visual cohesion with the host environment. + +#### Standardized Variables + +| Category | Variables | +|----------|-----------| +| Background | `--color-background-primary`, `--color-background-secondary`, `--color-background-tertiary`, `--color-background-inverted` | +| Text | `--color-text-primary`, `--color-text-secondary`, `--color-text-tertiary`, `--color-text-inverted` | +| Icons | `--color-icon-primary`, `--color-icon-secondary`, `--color-icon-tertiary`, `--color-icon-inverted` | +| Borders | `--color-border-primary`, `--color-border-secondary` | +| Accents | `--color-accent-info`, `--color-accent-danger`, `--color-accent-success`, `--color-accent-warning` | +| Font Family | `--font-family-sans` | +| Font Sizes | `--font-size-heading`, `--font-size-body`, `--font-size-caption` | +| Font Weights | `--font-weight-regular`, `--font-weight-emphasized` | +| Line Heights | `--font-leading-regular`, `--font-leading-tight` | +| Composite Styles | `--font-style-heading`, `--font-style-body`, `--font-style-body-emphasized`, `--font-style-caption`, `--font-style-caption-emphasized` | +| Border Radius | `--border-radius-small`, `--border-radius-medium`, `--border-radius-large`, `--border-radius-full` | +| Border Width | `--border-width-regular` | + +#### Host Behavior + +- Hosts MAY provide any subset of standardized variables +- Hosts MAY use CSS `light-dark()` function for theme-aware values +- `theme` indicates the active mode; `styles` provides the concrete CSS values + +#### App Behavior + +- Apps SHOULD use fallback values for CSS variables: `var(--color-text-primary, #171717)` +- This ensures graceful degradation when hosts omit `styles` or specific variables +- When the host uses `light-dark()` values, apps MUST set `color-scheme` on their document: + ```css + :root { color-scheme: light dark; } + ``` + +Example CSS: + +```css +.container { + background: var(--color-background-primary, #ffffff); + color: var(--color-text-primary, #171717); + font: var(--font-style-body, 400 16px/1.4 system-ui); +} +``` + ### MCP Apps Specific Messages MCP Apps introduces additional JSON-RPC methods for UI-specific functionality: @@ -1050,6 +1105,24 @@ This proposal synthesizes feedback from the UI CWG and MCP-UI community, host im - **Include external URLs in MVP:** This is one of the easiest content types for servers to adopt, as it's possible to embed regular apps. However, it was deferred due to concerns around model visibility, inability to screenshot content, and review process. - **Support multiple content types:** Deferred to maintain a lean MVP. +#### 4. Host Theming via CSS Variables + +**Decision:** Provide a standardized set of CSS custom properties for visual cohesion. + +**Rationale:** + +- CSS variables are universal, framework-agnostic, and require no runtime +- Apps apply styles via `var(--name)` with fallbacks for graceful degradation +- Limited variable set (colors, typography, borders) ensures hosts can realistically provide all values +- Spacing intentionally excluded—layouts break when spacing varies from original design +- No UI component library—no single library works across all host environments + +**Alternatives considered:** + +- **Full design system:** Rejected as too prescriptive; hosts have different aesthetics +- **Inline styles in tool results:** Rejected; separating theming from data enables caching and updates +- **CSS-in-JS injection:** Rejected; framework-specific and security concerns with injected code + ### Backward Compatibility The proposal builds on the existing core protocol. There are no incompatibilities. From aaedd858bbc5fbffb4601b010e941c97762b9611 Mon Sep 17 00:00:00 2001 From: martinalong Date: Thu, 11 Dec 2025 16:30:58 -0800 Subject: [PATCH 06/15] Suggested edits --- specification/draft/apps.mdx | 38 +++++++++++++++++------------------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/specification/draft/apps.mdx b/specification/draft/apps.mdx index 6f62ce688..2586d5267 100644 --- a/specification/draft/apps.mdx +++ b/specification/draft/apps.mdx @@ -440,10 +440,6 @@ interface HostContext { } ``` -All fields are optional. Hosts SHOULD provide relevant context. Guest UIs SHOULD handle missing fields gracefully. - -For `styles`, apps SHOULD provide CSS fallback values (e.g., `var(--color-text-primary, #171717)`) to handle hosts that don't supply styles. - Example: ```json @@ -460,7 +456,8 @@ Example: "styles": { "--color-background-primary": "light-dark(#ffffff, #171717)", "--color-text-primary": "light-dark(#171717, #fafafa)", - "--font-family-sans": "system-ui, sans-serif" + "--font-family-sans": "system-ui, sans-serif", + ... }, "displayMode": "inline", "viewport": { "width": 400, "height": 300 } @@ -471,9 +468,9 @@ Example: ### Theming -Hosts pass CSS custom properties via `HostContext.styles` for visual cohesion with the host environment. +Hosts can optionally pass CSS custom properties via `HostContext.styles` for visual cohesion with the host environment. -#### Standardized Variables +#### Current Standardized Variables | Category | Variables | |----------|-----------| @@ -492,26 +489,27 @@ Hosts pass CSS custom properties via `HostContext.styles` for visual cohesion wi #### Host Behavior -- Hosts MAY provide any subset of standardized variables -- Hosts MAY use CSS `light-dark()` function for theme-aware values -- `theme` indicates the active mode; `styles` provides the concrete CSS values +- Hosts can provide any subset of standardized variables, or not pass `styles` at all. However, passing all of the standardized properties is recommended for cohesiveness +- Hosts can use the CSS `light-dark()` function for theme-aware values #### App Behavior -- Apps SHOULD use fallback values for CSS variables: `var(--color-text-primary, #171717)` -- This ensures graceful degradation when hosts omit `styles` or specific variables -- When the host uses `light-dark()` values, apps MUST set `color-scheme` on their document: - ```css - :root { color-scheme: light dark; } - ``` +- Apps should set default fallback values for CSS variables, to account for hosts who don't pass some or all style variables. This ensures graceful degradation when hosts omit `styles` or specific variables: +``` +:root { + --colorTextPrimary: #171717; +} +``` +- Apps can use the `applyHostStyles` utility (or `useHostStyles` if they prefer a React hook) to easily populate the host-provided CSS variables into their style sheet +- Apps can use the `applyDocumentTheme` utility (or `useDocumentTheme` if they prefer a React hook) to easily respond to Host Context `theme` changes in a way that is compatible with the host's light/dark color variables -Example CSS: +Example usage of standardized CSS variables: ```css .container { - background: var(--color-background-primary, #ffffff); - color: var(--color-text-primary, #171717); - font: var(--font-style-body, 400 16px/1.4 system-ui); + background: var(--color-background-primary); + color: var(--color-text-primary); + font: var(--font-style-body); } ``` From db2872643c2ee7973740eeb900d03acad5e73bc7 Mon Sep 17 00:00:00 2001 From: martinalong Date: Fri, 12 Dec 2025 18:31:03 -0800 Subject: [PATCH 07/15] Make properties optional --- src/generated/schema.json | 46 ++++++++++++++++++++++++++++----------- src/generated/schema.ts | 18 ++++++++++++--- src/spec.types.ts | 8 ++++++- src/styles.ts | 4 +++- 4 files changed, 58 insertions(+), 18 deletions(-) diff --git a/src/generated/schema.json b/src/generated/schema.json index 63cb3d3b9..047ad2976 100644 --- a/src/generated/schema.json +++ b/src/generated/schema.json @@ -268,7 +268,7 @@ "description": "CSS variables for theming the app.", "type": "object", "propertyNames": { - "description": "Style variables for theming MCP apps.", + "description": "Style variables for theming MCP apps.\n\nIndividual style keys are optional - hosts may provide any subset of these values.\nValues are strings containing CSS values (colors, sizes, font stacks, etc.).\n\nNote: This type uses `Record` rather than `Partial>`\nfor compatibility with Zod schema generation. Both are functionally equivalent for validation.", "anyOf": [ { "type": "string", @@ -417,8 +417,13 @@ ] }, "additionalProperties": { - "description": "Style variables for theming MCP apps.", - "type": "string" + "description": "Style variables for theming MCP apps.\n\nIndividual style keys are optional - hosts may provide any subset of these values.\nValues are strings containing CSS values (colors, sizes, font stacks, etc.).\n\nNote: This type uses `Record` rather than `Partial>`\nfor compatibility with Zod schema generation. Both are functionally equivalent for validation.", + "anyOf": [ + { + "type": "string" + }, + {} + ] } }, "displayMode": { @@ -711,7 +716,7 @@ "description": "CSS variables for theming the app.", "type": "object", "propertyNames": { - "description": "Style variables for theming MCP apps.", + "description": "Style variables for theming MCP apps.\n\nIndividual style keys are optional - hosts may provide any subset of these values.\nValues are strings containing CSS values (colors, sizes, font stacks, etc.).\n\nNote: This type uses `Record` rather than `Partial>`\nfor compatibility with Zod schema generation. Both are functionally equivalent for validation.", "anyOf": [ { "type": "string", @@ -860,8 +865,13 @@ ] }, "additionalProperties": { - "description": "Style variables for theming MCP apps.", - "type": "string" + "description": "Style variables for theming MCP apps.\n\nIndividual style keys are optional - hosts may provide any subset of these values.\nValues are strings containing CSS values (colors, sizes, font stacks, etc.).\n\nNote: This type uses `Record` rather than `Partial>`\nfor compatibility with Zod schema generation. Both are functionally equivalent for validation.", + "anyOf": [ + { + "type": "string" + }, + {} + ] } }, "displayMode": { @@ -1336,7 +1346,7 @@ "description": "CSS variables for theming the app.", "type": "object", "propertyNames": { - "description": "Style variables for theming MCP apps.", + "description": "Style variables for theming MCP apps.\n\nIndividual style keys are optional - hosts may provide any subset of these values.\nValues are strings containing CSS values (colors, sizes, font stacks, etc.).\n\nNote: This type uses `Record` rather than `Partial>`\nfor compatibility with Zod schema generation. Both are functionally equivalent for validation.", "anyOf": [ { "type": "string", @@ -1485,8 +1495,13 @@ ] }, "additionalProperties": { - "description": "Style variables for theming MCP apps.", - "type": "string" + "description": "Style variables for theming MCP apps.\n\nIndividual style keys are optional - hosts may provide any subset of these values.\nValues are strings containing CSS values (colors, sizes, font stacks, etc.).\n\nNote: This type uses `Record` rather than `Partial>`\nfor compatibility with Zod schema generation. Both are functionally equivalent for validation.", + "anyOf": [ + { + "type": "string" + }, + {} + ] } }, "displayMode": { @@ -2341,10 +2356,10 @@ }, "McpUiStyles": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "description": "Style variables for theming MCP apps.", + "description": "Style variables for theming MCP apps.\n\nIndividual style keys are optional - hosts may provide any subset of these values.\nValues are strings containing CSS values (colors, sizes, font stacks, etc.).\n\nNote: This type uses `Record` rather than `Partial>`\nfor compatibility with Zod schema generation. Both are functionally equivalent for validation.", "type": "object", "propertyNames": { - "description": "Style variables for theming MCP apps.", + "description": "Style variables for theming MCP apps.\n\nIndividual style keys are optional - hosts may provide any subset of these values.\nValues are strings containing CSS values (colors, sizes, font stacks, etc.).\n\nNote: This type uses `Record` rather than `Partial>`\nfor compatibility with Zod schema generation. Both are functionally equivalent for validation.", "anyOf": [ { "type": "string", @@ -2493,8 +2508,13 @@ ] }, "additionalProperties": { - "description": "Style variables for theming MCP apps.", - "type": "string" + "description": "Style variables for theming MCP apps.\n\nIndividual style keys are optional - hosts may provide any subset of these values.\nValues are strings containing CSS values (colors, sizes, font stacks, etc.).\n\nNote: This type uses `Record` rather than `Partial>`\nfor compatibility with Zod schema generation. Both are functionally equivalent for validation.", + "anyOf": [ + { + "type": "string" + }, + {} + ] } }, "McpUiTheme": { diff --git a/src/generated/schema.ts b/src/generated/schema.ts index 6b804dc44..07b82cf35 100644 --- a/src/generated/schema.ts +++ b/src/generated/schema.ts @@ -70,15 +70,27 @@ export const McpUiStyleVariableKeySchema = z /** * @description Style variables for theming MCP apps. + * + * Individual style keys are optional - hosts may provide any subset of these values. + * Values are strings containing CSS values (colors, sizes, font stacks, etc.). + * + * Note: This type uses `Record` rather than `Partial>` + * for compatibility with Zod schema generation. Both are functionally equivalent for validation. */ export const McpUiStylesSchema = z .record( McpUiStyleVariableKeySchema.describe( - "Style variables for theming MCP apps.", + "Style variables for theming MCP apps.\n\nIndividual style keys are optional - hosts may provide any subset of these values.\nValues are strings containing CSS values (colors, sizes, font stacks, etc.).\n\nNote: This type uses `Record` rather than `Partial>`\nfor compatibility with Zod schema generation. Both are functionally equivalent for validation.", ), - z.string().describe("Style variables for theming MCP apps."), + z + .union([z.string(), z.undefined()]) + .describe( + "Style variables for theming MCP apps.\n\nIndividual style keys are optional - hosts may provide any subset of these values.\nValues are strings containing CSS values (colors, sizes, font stacks, etc.).\n\nNote: This type uses `Record` rather than `Partial>`\nfor compatibility with Zod schema generation. Both are functionally equivalent for validation.", + ), ) - .describe("Style variables for theming MCP apps."); + .describe( + "Style variables for theming MCP apps.\n\nIndividual style keys are optional - hosts may provide any subset of these values.\nValues are strings containing CSS values (colors, sizes, font stacks, etc.).\n\nNote: This type uses `Record` rather than `Partial>`\nfor compatibility with Zod schema generation. Both are functionally equivalent for validation.", + ); /** * @description Request to open an external URL in the host's default browser. diff --git a/src/spec.types.ts b/src/spec.types.ts index 0dbfb0355..5450f797a 100644 --- a/src/spec.types.ts +++ b/src/spec.types.ts @@ -91,8 +91,14 @@ export type McpUiStyleVariableKey = /** * @description Style variables for theming MCP apps. + * + * Individual style keys are optional - hosts may provide any subset of these values. + * Values are strings containing CSS values (colors, sizes, font stacks, etc.). + * + * Note: This type uses `Record` rather than `Partial>` + * for compatibility with Zod schema generation. Both are functionally equivalent for validation. */ -export type McpUiStyles = Record; +export type McpUiStyles = Record; /** * @description Request to open an external URL in the host's default browser. diff --git a/src/styles.ts b/src/styles.ts index 454424839..45957bef3 100644 --- a/src/styles.ts +++ b/src/styles.ts @@ -109,6 +109,8 @@ export function applyHostStyles( root: HTMLElement = document.documentElement, ): void { for (const [key, value] of Object.entries(styles)) { - root.style.setProperty(key, value); + if (value !== undefined) { + root.style.setProperty(key, value); + } } } From 35781865af82ffc4360876b08a132fc1fbbee310 Mon Sep 17 00:00:00 2001 From: martinalong Date: Fri, 12 Dec 2025 20:07:40 -0800 Subject: [PATCH 08/15] Add color-scheme meta tag to all example apps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prevents iframe from automatically applying a default background color. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- examples/basic-host/index.html | 1 + examples/basic-host/sandbox.html | 1 + examples/basic-server-react/mcp-app.html | 1 + examples/basic-server-vanillajs/mcp-app.html | 1 + examples/budget-allocator-server/mcp-app.html | 1 + examples/cohort-heatmap-server/mcp-app.html | 1 + examples/customer-segmentation-server/mcp-app.html | 1 + examples/qr-server/widget.html | 1 + examples/scenario-modeler-server/mcp-app.html | 1 + examples/system-monitor-server/mcp-app.html | 1 + examples/threejs-server/mcp-app.html | 1 + examples/wiki-explorer-server/mcp-app.html | 1 + 12 files changed, 12 insertions(+) diff --git a/examples/basic-host/index.html b/examples/basic-host/index.html index b865ebeed..68e8109f6 100644 --- a/examples/basic-host/index.html +++ b/examples/basic-host/index.html @@ -3,6 +3,7 @@ + MCP Apps Host diff --git a/examples/basic-host/sandbox.html b/examples/basic-host/sandbox.html index 3714e29b2..b868e582a 100644 --- a/examples/basic-host/sandbox.html +++ b/examples/basic-host/sandbox.html @@ -2,6 +2,7 @@ + MCP-UI Proxy diff --git a/examples/basic-server-react/mcp-app.html b/examples/basic-server-react/mcp-app.html index 771b73e25..205ff4e7a 100644 --- a/examples/basic-server-react/mcp-app.html +++ b/examples/basic-server-react/mcp-app.html @@ -3,6 +3,7 @@ + Get Time App diff --git a/examples/basic-server-vanillajs/mcp-app.html b/examples/basic-server-vanillajs/mcp-app.html index 1a88d60a4..5b6428014 100644 --- a/examples/basic-server-vanillajs/mcp-app.html +++ b/examples/basic-server-vanillajs/mcp-app.html @@ -3,6 +3,7 @@ + Get Time App diff --git a/examples/budget-allocator-server/mcp-app.html b/examples/budget-allocator-server/mcp-app.html index e57c1371b..d8724f4f7 100644 --- a/examples/budget-allocator-server/mcp-app.html +++ b/examples/budget-allocator-server/mcp-app.html @@ -3,6 +3,7 @@ + Budget Allocator diff --git a/examples/cohort-heatmap-server/mcp-app.html b/examples/cohort-heatmap-server/mcp-app.html index 2b1a47d3f..c9202a19a 100644 --- a/examples/cohort-heatmap-server/mcp-app.html +++ b/examples/cohort-heatmap-server/mcp-app.html @@ -3,6 +3,7 @@ + Cohort Retention Heatmap diff --git a/examples/customer-segmentation-server/mcp-app.html b/examples/customer-segmentation-server/mcp-app.html index 10e584d19..33a684a99 100644 --- a/examples/customer-segmentation-server/mcp-app.html +++ b/examples/customer-segmentation-server/mcp-app.html @@ -3,6 +3,7 @@ + Customer Segmentation Explorer diff --git a/examples/qr-server/widget.html b/examples/qr-server/widget.html index 3c0be72d1..e2ff4cb0a 100644 --- a/examples/qr-server/widget.html +++ b/examples/qr-server/widget.html @@ -1,6 +1,7 @@ +