Merge pull request #127 from modelcontextprotocol/martinalong/mcp-apps/add-styles
[MCP Apps] Add `styles` prop to host context
This commit is contained in:
commit
e316259f58
27 changed files with 3128 additions and 408 deletions
|
|
@ -3,6 +3,7 @@
|
|||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<title>MCP Apps Host</title>
|
||||
<link rel="stylesheet" href="/src/global.css">
|
||||
</head>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<!-- CSP is set by serve.ts HTTP header - no meta tag needed here
|
||||
The inner iframe's CSP is dynamically injected based on resource metadata -->
|
||||
<title>MCP-UI Proxy</title>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<title>Get Time App</title>
|
||||
<link rel="stylesheet" href="/src/global.css">
|
||||
</head>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<title>Get Time App</title>
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<title>Budget Allocator</title>
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<title>Cohort Retention Heatmap</title>
|
||||
<link rel="stylesheet" href="/src/global.css">
|
||||
</head>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<title>Customer Segmentation Explorer</title>
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
|||
|
|
@ -1,42 +1,45 @@
|
|||
/* Default fallback values for host style variables */
|
||||
:root {
|
||||
--color-bg: #ffffff;
|
||||
--color-text: #1f2937;
|
||||
--color-text-muted: #6b7280;
|
||||
--color-primary: #2563eb;
|
||||
--color-primary-hover: #1d4ed8;
|
||||
--color-card-bg: #f9fafb;
|
||||
--color-border: #e5e7eb;
|
||||
--color-enterprise: #1e40af;
|
||||
--color-midmarket: #0d9488;
|
||||
--color-smb: #059669;
|
||||
--color-startup: #6366f1;
|
||||
}
|
||||
color-scheme: light dark;
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--color-bg: #111827;
|
||||
--color-text: #f9fafb;
|
||||
--color-text-muted: #9ca3af;
|
||||
--color-primary: #3b82f6;
|
||||
--color-primary-hover: #60a5fa;
|
||||
--color-card-bg: #1f2937;
|
||||
--color-border: #374151;
|
||||
--color-enterprise: #3b82f6;
|
||||
--color-midmarket: #14b8a6;
|
||||
--color-smb: #10b981;
|
||||
--color-startup: #818cf8;
|
||||
}
|
||||
/* Background colors */
|
||||
--color-background-primary: light-dark(#ffffff, #111827);
|
||||
--color-background-secondary: light-dark(#f9fafb, #1f2937);
|
||||
--color-background-tertiary: light-dark(#f3f4f6, #374151);
|
||||
|
||||
/* Text colors */
|
||||
--color-text-primary: light-dark(#1f2937, #f9fafb);
|
||||
--color-text-secondary: light-dark(#6b7280, #9ca3af);
|
||||
--color-text-tertiary: light-dark(#9ca3af, #6b7280);
|
||||
|
||||
/* Border colors */
|
||||
--color-border-primary: light-dark(#e5e7eb, #374151);
|
||||
--color-border-secondary: light-dark(#d1d5db, #4b5563);
|
||||
|
||||
/* Accent colors */
|
||||
--color-accent-info: light-dark(#2563eb, #3b82f6);
|
||||
|
||||
/* Border radius */
|
||||
--border-radius-sm: 6px;
|
||||
--border-radius-md: 8px;
|
||||
|
||||
/* App-specific colors (not from host) */
|
||||
--color-enterprise: light-dark(#1e40af, #3b82f6);
|
||||
--color-midmarket: light-dark(#0d9488, #14b8a6);
|
||||
--color-smb: light-dark(#059669, #10b981);
|
||||
--color-startup: light-dark(#6366f1, #818cf8);
|
||||
}
|
||||
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
color: var(--color-text-primary);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.main {
|
||||
border-radius: var(--border-radius-md);
|
||||
background: var(--color-background-primary);
|
||||
width: 100%;
|
||||
height: 600px;
|
||||
margin: 0 auto;
|
||||
|
|
@ -85,21 +88,21 @@ html, body {
|
|||
gap: 6px;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-muted);
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.select {
|
||||
padding: 4px 8px;
|
||||
font-size: 0.8125rem;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 4px;
|
||||
background: var(--color-card-bg);
|
||||
color: var(--color-text);
|
||||
border: 1px solid var(--color-border-primary);
|
||||
border-radius: var(--border-radius-sm);
|
||||
background: var(--color-background-secondary);
|
||||
color: var(--color-text-primary);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.select:focus {
|
||||
outline: 2px solid var(--color-primary);
|
||||
outline: 2px solid var(--color-accent-info);
|
||||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
|
|
@ -107,10 +110,10 @@ html, body {
|
|||
.chart-section {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
background: var(--color-card-bg);
|
||||
border-radius: 8px;
|
||||
background: var(--color-background-secondary);
|
||||
border-radius: var(--border-radius-md);
|
||||
padding: 8px;
|
||||
border: 1px solid var(--color-border);
|
||||
border: 1px solid var(--color-border-primary);
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
|
|
@ -141,13 +144,13 @@ html, body {
|
|||
cursor: pointer;
|
||||
padding: 4px 10px;
|
||||
border-radius: 16px;
|
||||
border: 1px solid var(--color-border);
|
||||
background: var(--color-card-bg);
|
||||
border: 1px solid var(--color-border-primary);
|
||||
background: var(--color-background-secondary);
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.legend-item:hover {
|
||||
border-color: var(--color-text-muted);
|
||||
border-color: var(--color-text-secondary);
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
|
|
@ -171,7 +174,7 @@ html, body {
|
|||
}
|
||||
|
||||
.legend-count {
|
||||
color: var(--color-text-muted);
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
/* Detail section - ~44px */
|
||||
|
|
@ -186,15 +189,15 @@ html, body {
|
|||
justify-content: center;
|
||||
gap: 16px;
|
||||
height: 100%;
|
||||
background: var(--color-card-bg);
|
||||
border-radius: 6px;
|
||||
background: var(--color-background-secondary);
|
||||
border-radius: var(--border-radius-sm);
|
||||
padding: 0 12px;
|
||||
border: 1px solid var(--color-border);
|
||||
border: 1px solid var(--color-border-primary);
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.detail-placeholder {
|
||||
color: var(--color-text-muted);
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.detail-name {
|
||||
|
|
@ -203,7 +206,7 @@ html, body {
|
|||
|
||||
.detail-segment {
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
border-radius: var(--border-radius-sm);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
color: white;
|
||||
|
|
@ -215,10 +218,10 @@ html, body {
|
|||
.detail-segment.startup { background: var(--color-startup); }
|
||||
|
||||
.detail-metric {
|
||||
color: var(--color-text-muted);
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.detail-metric strong {
|
||||
color: var(--color-text);
|
||||
color: var(--color-text-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,12 @@
|
|||
/**
|
||||
* @file Customer Segmentation Explorer - interactive scatter/bubble visualization
|
||||
*/
|
||||
import { App, PostMessageTransport } from "@modelcontextprotocol/ext-apps";
|
||||
import {
|
||||
App,
|
||||
PostMessageTransport,
|
||||
applyHostStyleVariables,
|
||||
applyDocumentTheme,
|
||||
} from "@modelcontextprotocol/ext-apps";
|
||||
import { Chart, registerables } from "chart.js";
|
||||
import "./global.css";
|
||||
import "./mcp-app.css";
|
||||
|
|
@ -138,11 +143,40 @@ function buildDatasets(): Chart["data"]["datasets"] {
|
|||
});
|
||||
}
|
||||
|
||||
// Hidden element for resolving CSS color values (reused to avoid DOM thrashing)
|
||||
let colorResolver: HTMLDivElement | null = null;
|
||||
|
||||
// Resolve a CSS color value (handles light-dark() function)
|
||||
function resolveColor(cssValue: string, fallback: string): string {
|
||||
if (!cssValue) return fallback;
|
||||
// If it's a simple color value, return it directly
|
||||
if (!cssValue.includes("light-dark(")) return cssValue;
|
||||
// Create resolver element once and keep it hidden
|
||||
if (!colorResolver) {
|
||||
colorResolver = document.createElement("div");
|
||||
colorResolver.style.position = "absolute";
|
||||
colorResolver.style.visibility = "hidden";
|
||||
colorResolver.style.pointerEvents = "none";
|
||||
document.body.appendChild(colorResolver);
|
||||
}
|
||||
colorResolver.style.color = cssValue;
|
||||
return getComputedStyle(colorResolver).color || fallback;
|
||||
}
|
||||
|
||||
// Get colors from CSS variables
|
||||
function getChartColors(): { textColor: string; gridColor: string } {
|
||||
const style = getComputedStyle(document.documentElement);
|
||||
const rawTextColor = style.getPropertyValue("--color-text-secondary").trim();
|
||||
const rawGridColor = style.getPropertyValue("--color-border-primary").trim();
|
||||
return {
|
||||
textColor: resolveColor(rawTextColor, "#6b7280"),
|
||||
gridColor: resolveColor(rawGridColor, "#e5e7eb"),
|
||||
};
|
||||
}
|
||||
|
||||
// Initialize Chart.js
|
||||
function initChart(): Chart {
|
||||
const isDarkMode = window.matchMedia("(prefers-color-scheme: dark)").matches;
|
||||
const textColor = isDarkMode ? "#9ca3af" : "#6b7280";
|
||||
const gridColor = isDarkMode ? "#374151" : "#e5e7eb";
|
||||
const { textColor, gridColor } = getChartColors();
|
||||
|
||||
return new Chart(chartCanvas, {
|
||||
type: "bubble",
|
||||
|
|
@ -243,8 +277,7 @@ function initChart(): Chart {
|
|||
function updateChart(): void {
|
||||
if (!state.chart) return;
|
||||
|
||||
const isDarkMode = window.matchMedia("(prefers-color-scheme: dark)").matches;
|
||||
const textColor = isDarkMode ? "#9ca3af" : "#6b7280";
|
||||
const { textColor, gridColor } = getChartColors();
|
||||
|
||||
state.chart.data.datasets = buildDatasets();
|
||||
|
||||
|
|
@ -252,11 +285,13 @@ function updateChart(): void {
|
|||
const scales = state.chart.options.scales as {
|
||||
x: {
|
||||
title: { text: string; color: string };
|
||||
ticks: { callback: (value: number) => string };
|
||||
ticks: { color: string; callback: (value: number) => string };
|
||||
grid: { color: string };
|
||||
};
|
||||
y: {
|
||||
title: { text: string; color: string };
|
||||
ticks: { callback: (value: number) => string };
|
||||
ticks: { color: string; callback: (value: number) => string };
|
||||
grid: { color: string };
|
||||
};
|
||||
};
|
||||
|
||||
|
|
@ -264,8 +299,12 @@ function updateChart(): void {
|
|||
scales.y.title.text = METRIC_LABELS[state.yAxis];
|
||||
scales.x.title.color = textColor;
|
||||
scales.y.title.color = textColor;
|
||||
scales.x.ticks.color = textColor;
|
||||
scales.y.ticks.color = textColor;
|
||||
scales.x.ticks.callback = (value: number) => formatValue(value, state.xAxis);
|
||||
scales.y.ticks.callback = (value: number) => formatValue(value, state.yAxis);
|
||||
scales.x.grid.color = gridColor;
|
||||
scales.y.grid.color = gridColor;
|
||||
|
||||
state.chart.update();
|
||||
}
|
||||
|
|
@ -388,20 +427,52 @@ document.addEventListener("click", (e) => {
|
|||
}
|
||||
});
|
||||
|
||||
// Handle theme changes
|
||||
// Handle system theme changes (fallback when host doesn't provide styles)
|
||||
window
|
||||
.matchMedia("(prefers-color-scheme: dark)")
|
||||
.addEventListener("change", () => {
|
||||
if (state.chart) {
|
||||
state.chart.destroy();
|
||||
state.chart = initChart();
|
||||
.addEventListener("change", (e) => {
|
||||
// Only apply if we haven't received host theme
|
||||
if (!app.getHostContext()?.theme) {
|
||||
applyDocumentTheme(e.matches ? "dark" : "light");
|
||||
if (state.chart) {
|
||||
state.chart.destroy();
|
||||
state.chart = initChart();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Apply initial theme based on system preference (before host context is available)
|
||||
const systemDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
|
||||
applyDocumentTheme(systemDark ? "dark" : "light");
|
||||
|
||||
// Register handlers and connect
|
||||
app.onerror = log.error;
|
||||
|
||||
app.connect(new PostMessageTransport(window.parent));
|
||||
// Handle host context changes (theme and styles from host)
|
||||
app.onhostcontextchanged = (params) => {
|
||||
if (params.theme) {
|
||||
applyDocumentTheme(params.theme);
|
||||
}
|
||||
if (params.styles?.variables) {
|
||||
applyHostStyleVariables(params.styles.variables);
|
||||
}
|
||||
// Recreate chart to pick up new colors
|
||||
if (state.chart && (params.theme || params.styles?.variables)) {
|
||||
state.chart.destroy();
|
||||
state.chart = initChart();
|
||||
}
|
||||
};
|
||||
|
||||
app.connect(new PostMessageTransport(window.parent)).then(() => {
|
||||
// Apply initial host context after connection
|
||||
const ctx = app.getHostContext();
|
||||
if (ctx?.theme) {
|
||||
applyDocumentTheme(ctx.theme);
|
||||
}
|
||||
if (ctx?.styles?.variables) {
|
||||
applyHostStyleVariables(ctx.styles.variables);
|
||||
}
|
||||
});
|
||||
|
||||
// Fetch data after connection
|
||||
setTimeout(fetchData, 100);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<style>
|
||||
html, body {
|
||||
margin: 0;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<title>SaaS Scenario Modeler</title>
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<title>System Monitor</title>
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<title>Three.js Widget</title>
|
||||
<link rel="stylesheet" href="/src/global.css">
|
||||
</head>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<title>Wiki Explorer</title>
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
|||
|
|
@ -154,11 +154,12 @@ interface UIResourceMeta {
|
|||
/**
|
||||
* Visual boundary preference
|
||||
*
|
||||
* Boolean indicating the UI prefers a visible border. Useful for widgets
|
||||
* that might blend with host background.
|
||||
* Boolean controlling whether a visible border and background is provided by the host. Specifying an
|
||||
* explicit value for this is recommended because hosts' defaults may vary.
|
||||
*
|
||||
* - `true`: Request visible border (host decides styling)
|
||||
* - `false` or omitted: No preference
|
||||
* - `true`: Request visible border + background
|
||||
* - `false`: Request no visible border + background
|
||||
* - omitted: host decides border
|
||||
*/
|
||||
prefersBorder?: boolean,
|
||||
}
|
||||
|
|
@ -391,7 +392,7 @@ UI iframes can use the following subset of standard MCP protocol messages:
|
|||
|
||||
### Host Context in `McpUiInitializeResult`
|
||||
|
||||
When the Guest UI sends an `ui/initialize` request to the Host, the Host SHOULD include UI-specific context in the `McpUiInitializeResult`’s `hostContext` field:
|
||||
When the Guest UI sends an `ui/initialize` request to the Host, the Host SHOULD include UI-specific context in the `McpUiInitializeResult`'s `hostContext` field:
|
||||
|
||||
```typescript
|
||||
interface HostContext {
|
||||
|
|
@ -404,6 +405,11 @@ interface HostContext {
|
|||
};
|
||||
/** Current color theme preference */
|
||||
theme?: "light" | "dark";
|
||||
/** Style configuration for theming. See Theming section for standardized variable names. */
|
||||
styles?: {
|
||||
/** CSS variables for theming */
|
||||
variables?: Record<McpUiStyleVariableKey, string | undefined>;
|
||||
};
|
||||
/** How the UI is currently displayed */
|
||||
displayMode?: "inline" | "fullscreen" | "pip";
|
||||
/** Display modes the host supports */
|
||||
|
|
@ -438,8 +444,6 @@ interface HostContext {
|
|||
}
|
||||
```
|
||||
|
||||
All fields are optional. Hosts SHOULD provide relevant context. Guest UIs SHOULD handle missing fields gracefully.
|
||||
|
||||
Example:
|
||||
|
||||
```json
|
||||
|
|
@ -453,6 +457,14 @@ Example:
|
|||
"hostInfo": { "name": "claude-desktop", "version": "1.0.0" },
|
||||
"hostContext": {
|
||||
"theme": "dark",
|
||||
"styles": {
|
||||
"variables": {
|
||||
"--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 +472,134 @@ Example:
|
|||
}
|
||||
```
|
||||
|
||||
### Theming
|
||||
|
||||
Hosts can optionally pass CSS custom properties via `HostContext.styles.variables` for visual cohesion with the host environment.
|
||||
|
||||
#### Current Standardized Variables
|
||||
|
||||
```typescript
|
||||
/** CSS variable keys available to MCP apps for theming. */
|
||||
type McpUiStyleVariableKey =
|
||||
// Background colors
|
||||
| "--color-background-primary"
|
||||
| "--color-background-secondary"
|
||||
| "--color-background-tertiary"
|
||||
| "--color-background-inverse"
|
||||
| "--color-background-ghost"
|
||||
| "--color-background-info"
|
||||
| "--color-background-danger"
|
||||
| "--color-background-success"
|
||||
| "--color-background-warning"
|
||||
| "--color-background-disabled"
|
||||
// Text colors
|
||||
| "--color-text-primary"
|
||||
| "--color-text-secondary"
|
||||
| "--color-text-tertiary"
|
||||
| "--color-text-inverse"
|
||||
| "--color-text-info"
|
||||
| "--color-text-danger"
|
||||
| "--color-text-success"
|
||||
| "--color-text-warning"
|
||||
| "--color-text-disabled"
|
||||
// Border colors
|
||||
| "--color-border-primary"
|
||||
| "--color-border-secondary"
|
||||
| "--color-border-tertiary"
|
||||
| "--color-border-inverse"
|
||||
| "--color-border-ghost"
|
||||
| "--color-border-info"
|
||||
| "--color-border-danger"
|
||||
| "--color-border-success"
|
||||
| "--color-border-warning"
|
||||
| "--color-border-disabled"
|
||||
// Ring colors
|
||||
| "--color-ring-primary"
|
||||
| "--color-ring-secondary"
|
||||
| "--color-ring-inverse"
|
||||
| "--color-ring-info"
|
||||
| "--color-ring-danger"
|
||||
| "--color-ring-success"
|
||||
| "--color-ring-warning"
|
||||
// Typography - Family
|
||||
| "--font-sans"
|
||||
| "--font-mono"
|
||||
// Typography - Weight
|
||||
| "--font-weight-normal"
|
||||
| "--font-weight-medium"
|
||||
| "--font-weight-semibold"
|
||||
| "--font-weight-bold"
|
||||
// Typography - Text Size
|
||||
| "--font-text-xs-size"
|
||||
| "--font-text-sm-size"
|
||||
| "--font-text-md-size"
|
||||
| "--font-text-lg-size"
|
||||
// Typography - Heading Size
|
||||
| "--font-heading-xs-size"
|
||||
| "--font-heading-sm-size"
|
||||
| "--font-heading-md-size"
|
||||
| "--font-heading-lg-size"
|
||||
| "--font-heading-xl-size"
|
||||
| "--font-heading-2xl-size"
|
||||
| "--font-heading-3xl-size"
|
||||
// Typography - Text Line Height
|
||||
| "--font-text-xs-line-height"
|
||||
| "--font-text-sm-line-height"
|
||||
| "--font-text-md-line-height"
|
||||
| "--font-text-lg-line-height"
|
||||
// Typography - Heading Line Height
|
||||
| "--font-heading-xs-line-height"
|
||||
| "--font-heading-sm-line-height"
|
||||
| "--font-heading-md-line-height"
|
||||
| "--font-heading-lg-line-height"
|
||||
| "--font-heading-xl-line-height"
|
||||
| "--font-heading-2xl-line-height"
|
||||
| "--font-heading-3xl-line-height"
|
||||
// Border radius
|
||||
| "--border-radius-xs"
|
||||
| "--border-radius-sm"
|
||||
| "--border-radius-md"
|
||||
| "--border-radius-lg"
|
||||
| "--border-radius-xl"
|
||||
| "--border-radius-full"
|
||||
// Border width
|
||||
| "--border-width-regular"
|
||||
// Shadows
|
||||
| "--shadow-hairline"
|
||||
| "--shadow-sm"
|
||||
| "--shadow-md"
|
||||
| "--shadow-lg";
|
||||
```
|
||||
|
||||
#### Host Behavior
|
||||
|
||||
- Hosts can provide any subset of standardized variables, or not pass `styles` at all
|
||||
- However, unexpected clashes may occur if hosts pass some color variables but not others for example, since apps are instructed to fallback on their own default values for unspecified style variables
|
||||
- Hosts should use the CSS `light-dark()` function for theme-aware values (i.e. light mode and dark mode colors)
|
||||
|
||||
#### App Behavior
|
||||
|
||||
- Apps should set default fallback values for the set of these variables that they use, 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 {
|
||||
--color-text-primary: light-dark(#171717, #000000);
|
||||
--border-radius-small: 8px;
|
||||
...
|
||||
}
|
||||
```
|
||||
- Apps can use the `applyHostStyleVariables` utility (or `useHostStyleVariables` 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 usage of standardized CSS variables:
|
||||
|
||||
```css
|
||||
.container {
|
||||
background: var(--color-background-primary);
|
||||
color: var(--color-text-primary);
|
||||
font: var(--font-style-body);
|
||||
}
|
||||
```
|
||||
|
||||
### MCP Apps Specific Messages
|
||||
|
||||
MCP Apps introduces additional JSON-RPC methods for UI-specific functionality:
|
||||
|
|
@ -1050,6 +1190,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.
|
||||
|
|
|
|||
|
|
@ -47,6 +47,11 @@ import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
|
|||
|
||||
export { PostMessageTransport } from "./message-transport";
|
||||
export * from "./types";
|
||||
export {
|
||||
applyHostStyleVariables,
|
||||
getDocumentTheme,
|
||||
applyDocumentTheme,
|
||||
} from "./styles";
|
||||
|
||||
/**
|
||||
* Metadata key for associating a resource URI with a tool call.
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -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
|
||||
>;
|
||||
|
|
@ -55,6 +63,10 @@ export type McpUiToolCancelledNotificationSchemaInferredType = z.infer<
|
|||
typeof generated.McpUiToolCancelledNotificationSchema
|
||||
>;
|
||||
|
||||
export type McpUiHostStylesSchemaInferredType = z.infer<
|
||||
typeof generated.McpUiHostStylesSchema
|
||||
>;
|
||||
|
||||
export type McpUiResourceTeardownRequestSchemaInferredType = z.infer<
|
||||
typeof generated.McpUiResourceTeardownRequestSchema
|
||||
>;
|
||||
|
|
@ -111,6 +123,14 @@ expectType<spec.McpUiTheme>({} as McpUiThemeSchemaInferredType);
|
|||
expectType<McpUiThemeSchemaInferredType>({} as spec.McpUiTheme);
|
||||
expectType<spec.McpUiDisplayMode>({} as McpUiDisplayModeSchemaInferredType);
|
||||
expectType<McpUiDisplayModeSchemaInferredType>({} as spec.McpUiDisplayMode);
|
||||
expectType<spec.McpUiStyleVariableKey>(
|
||||
{} as McpUiStyleVariableKeySchemaInferredType,
|
||||
);
|
||||
expectType<McpUiStyleVariableKeySchemaInferredType>(
|
||||
{} as spec.McpUiStyleVariableKey,
|
||||
);
|
||||
expectType<spec.McpUiStyles>({} as McpUiStylesSchemaInferredType);
|
||||
expectType<McpUiStylesSchemaInferredType>({} as spec.McpUiStyles);
|
||||
expectType<spec.McpUiOpenLinkRequest>(
|
||||
{} as McpUiOpenLinkRequestSchemaInferredType,
|
||||
);
|
||||
|
|
@ -161,6 +181,8 @@ expectType<spec.McpUiToolCancelledNotification>(
|
|||
expectType<McpUiToolCancelledNotificationSchemaInferredType>(
|
||||
{} as spec.McpUiToolCancelledNotification,
|
||||
);
|
||||
expectType<spec.McpUiHostStyles>({} as McpUiHostStylesSchemaInferredType);
|
||||
expectType<McpUiHostStylesSchemaInferredType>({} as spec.McpUiHostStyles);
|
||||
expectType<spec.McpUiResourceTeardownRequest>(
|
||||
{} as McpUiResourceTeardownRequestSchemaInferredType,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -24,6 +24,113 @@ 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-inverse"),
|
||||
z.literal("--color-background-ghost"),
|
||||
z.literal("--color-background-info"),
|
||||
z.literal("--color-background-danger"),
|
||||
z.literal("--color-background-success"),
|
||||
z.literal("--color-background-warning"),
|
||||
z.literal("--color-background-disabled"),
|
||||
z.literal("--color-text-primary"),
|
||||
z.literal("--color-text-secondary"),
|
||||
z.literal("--color-text-tertiary"),
|
||||
z.literal("--color-text-inverse"),
|
||||
z.literal("--color-text-info"),
|
||||
z.literal("--color-text-danger"),
|
||||
z.literal("--color-text-success"),
|
||||
z.literal("--color-text-warning"),
|
||||
z.literal("--color-text-disabled"),
|
||||
z.literal("--color-border-primary"),
|
||||
z.literal("--color-border-secondary"),
|
||||
z.literal("--color-border-tertiary"),
|
||||
z.literal("--color-border-inverse"),
|
||||
z.literal("--color-border-ghost"),
|
||||
z.literal("--color-border-info"),
|
||||
z.literal("--color-border-danger"),
|
||||
z.literal("--color-border-success"),
|
||||
z.literal("--color-border-warning"),
|
||||
z.literal("--color-border-disabled"),
|
||||
z.literal("--color-ring-primary"),
|
||||
z.literal("--color-ring-secondary"),
|
||||
z.literal("--color-ring-inverse"),
|
||||
z.literal("--color-ring-info"),
|
||||
z.literal("--color-ring-danger"),
|
||||
z.literal("--color-ring-success"),
|
||||
z.literal("--color-ring-warning"),
|
||||
z.literal("--font-sans"),
|
||||
z.literal("--font-mono"),
|
||||
z.literal("--font-weight-normal"),
|
||||
z.literal("--font-weight-medium"),
|
||||
z.literal("--font-weight-semibold"),
|
||||
z.literal("--font-weight-bold"),
|
||||
z.literal("--font-text-xs-size"),
|
||||
z.literal("--font-text-sm-size"),
|
||||
z.literal("--font-text-md-size"),
|
||||
z.literal("--font-text-lg-size"),
|
||||
z.literal("--font-heading-xs-size"),
|
||||
z.literal("--font-heading-sm-size"),
|
||||
z.literal("--font-heading-md-size"),
|
||||
z.literal("--font-heading-lg-size"),
|
||||
z.literal("--font-heading-xl-size"),
|
||||
z.literal("--font-heading-2xl-size"),
|
||||
z.literal("--font-heading-3xl-size"),
|
||||
z.literal("--font-text-xs-line-height"),
|
||||
z.literal("--font-text-sm-line-height"),
|
||||
z.literal("--font-text-md-line-height"),
|
||||
z.literal("--font-text-lg-line-height"),
|
||||
z.literal("--font-heading-xs-line-height"),
|
||||
z.literal("--font-heading-sm-line-height"),
|
||||
z.literal("--font-heading-md-line-height"),
|
||||
z.literal("--font-heading-lg-line-height"),
|
||||
z.literal("--font-heading-xl-line-height"),
|
||||
z.literal("--font-heading-2xl-line-height"),
|
||||
z.literal("--font-heading-3xl-line-height"),
|
||||
z.literal("--border-radius-xs"),
|
||||
z.literal("--border-radius-sm"),
|
||||
z.literal("--border-radius-md"),
|
||||
z.literal("--border-radius-lg"),
|
||||
z.literal("--border-radius-xl"),
|
||||
z.literal("--border-radius-full"),
|
||||
z.literal("--border-width-regular"),
|
||||
z.literal("--shadow-hairline"),
|
||||
z.literal("--shadow-sm"),
|
||||
z.literal("--shadow-md"),
|
||||
z.literal("--shadow-lg"),
|
||||
])
|
||||
.describe("CSS variable keys available to MCP apps for theming.");
|
||||
|
||||
/**
|
||||
* @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<K, string | undefined>` rather than `Partial<Record<K, string>>`
|
||||
* 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.\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<K, string | undefined>` rather than `Partial<Record<K, string>>`\nfor compatibility with Zod schema generation. Both are functionally equivalent for validation.",
|
||||
),
|
||||
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<K, string | undefined>` rather than `Partial<Record<K, string>>`\nfor compatibility with Zod schema generation. Both are functionally equivalent for validation.",
|
||||
),
|
||||
)
|
||||
.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<K, string | undefined>` rather than `Partial<Record<K, string>>`\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.
|
||||
* @see {@link app.App.sendOpenLink} for the method that sends this request
|
||||
|
|
@ -178,6 +285,16 @@ export const McpUiToolCancelledNotificationSchema = z.object({
|
|||
}),
|
||||
});
|
||||
|
||||
/**
|
||||
* @description Style configuration for theming MCP apps.
|
||||
*/
|
||||
export const McpUiHostStylesSchema = z.object({
|
||||
/** @description CSS variables for theming the app. */
|
||||
variables: McpUiStylesSchema.optional().describe(
|
||||
"CSS variables for theming the app.",
|
||||
),
|
||||
});
|
||||
|
||||
/**
|
||||
* @description Request for graceful shutdown of the Guest UI (Host -> Guest UI).
|
||||
* @see {@link app-bridge.AppBridge.sendResourceTeardown} for the host method that sends this
|
||||
|
|
@ -354,6 +471,10 @@ export const McpUiHostContextSchema = z.looseObject({
|
|||
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.",
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@
|
|||
* ## Main Exports
|
||||
*
|
||||
* - {@link useApp} - React hook to create and connect an MCP App
|
||||
* - {@link useHostStyleVariables} - React hook to apply host style variables and theme
|
||||
* - {@link useDocumentTheme} - React hook for reactive document theme
|
||||
* - {@link useAutoResize} - React hook for manual auto-resize control (rarely needed)
|
||||
*
|
||||
* @module @modelcontextprotocol/ext-apps/react
|
||||
|
|
@ -32,3 +34,5 @@
|
|||
*/
|
||||
export * from "./useApp";
|
||||
export * from "./useAutoResize";
|
||||
export * from "./useDocumentTheme";
|
||||
export * from "./useHostStyles";
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue