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 @@
+