diff --git a/src/generated/schema.json b/src/generated/schema.json index f70b60f..63cb3d3 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 051ac8e..3576dc2 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 baff965..6b804dc 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 9fa7ad5..0a2e32c 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); }