Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions examples/basic-host/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -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">
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made this change to all the examples so that they can properly have a transparent background. Without it, iframe sets a default black/white background

<title>MCP Apps Host</title>
<link rel="stylesheet" href="/src/global.css">
</head>
Expand Down
1 change: 1 addition & 0 deletions examples/basic-host/sandbox.html
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
1 change: 1 addition & 0 deletions examples/basic-server-react/mcp-app.html
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
1 change: 1 addition & 0 deletions examples/basic-server-vanillajs/mcp-app.html
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
1 change: 1 addition & 0 deletions examples/budget-allocator-server/mcp-app.html
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
1 change: 1 addition & 0 deletions examples/cohort-heatmap-server/mcp-app.html
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
1 change: 1 addition & 0 deletions examples/customer-segmentation-server/mcp-app.html
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
101 changes: 52 additions & 49 deletions examples/customer-segmentation-server/src/mcp-app.css
Original file line number Diff line number Diff line change
@@ -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;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes in this file are me updating this one example to use the host context styles (with its original styles as fallbacks)


/* 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;
Expand Down Expand Up @@ -85,32 +88,32 @@ 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;
}

/* Chart section - ~420px */
.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 {
Expand Down Expand Up @@ -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);
}

Expand All @@ -171,7 +174,7 @@ html, body {
}

.legend-count {
color: var(--color-text-muted);
color: var(--color-text-secondary);
}

/* Detail section - ~44px */
Expand All @@ -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 {
Expand All @@ -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;
Expand All @@ -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;
}
99 changes: 85 additions & 14 deletions examples/customer-segmentation-server/src/mcp-app.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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 {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is necessary for this particular example b/c chartJS doesn't play nicely with light-dark by default

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",
Expand Down Expand Up @@ -243,29 +277,34 @@ 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();

// Update axis titles and formatters (using type assertions for Chart.js scale options)
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 };
};
};

scales.x.title.text = METRIC_LABELS[state.xAxis];
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();
}
Expand Down Expand Up @@ -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);
Loading
Loading