diff --git a/examples/basic-host/index.html b/examples/basic-host/index.html index b865ebee..68e8109f 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 3714e29b..b868e582 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 771b73e2..205ff4e7 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 1a88d60a..5b642801 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 e57c1371..d8724f4f 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 2b1a47d3..c9202a19 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 10e584d1..33a684a9 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/customer-segmentation-server/src/mcp-app.css b/examples/customer-segmentation-server/src/mcp-app.css index 4bdede2c..6bb5df4f 100644 --- a/examples/customer-segmentation-server/src/mcp-app.css +++ b/examples/customer-segmentation-server/src/mcp-app.css @@ -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; -} - -@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; - } + color-scheme: light dark; + + /* 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; } diff --git a/examples/customer-segmentation-server/src/mcp-app.ts b/examples/customer-segmentation-server/src/mcp-app.ts index 757937cb..77e348de 100644 --- a/examples/customer-segmentation-server/src/mcp-app.ts +++ b/examples/customer-segmentation-server/src/mcp-app.ts @@ -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); diff --git a/examples/qr-server/widget.html b/examples/qr-server/widget.html index 3c0be72d..e2ff4cb0 100644 --- a/examples/qr-server/widget.html +++ b/examples/qr-server/widget.html @@ -1,6 +1,7 @@ +