Skip to content

Commit e316259

Browse files
authored
Merge pull request #127 from modelcontextprotocol/martinalong/mcp-apps/add-styles
[MCP Apps] Add `styles` prop to host context
2 parents 6f14cf3 + 8f591b6 commit e316259

File tree

27 files changed

+3128
-408
lines changed

27 files changed

+3128
-408
lines changed

examples/basic-host/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
<head>
44
<meta charset="UTF-8">
55
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<meta name="color-scheme" content="light dark">
67
<title>MCP Apps Host</title>
78
<link rel="stylesheet" href="/src/global.css">
89
</head>

examples/basic-host/sandbox.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
<html>
33
<head>
44
<meta charset="utf-8" />
5+
<meta name="color-scheme" content="light dark">
56
<!-- CSP is set by serve.ts HTTP header - no meta tag needed here
67
The inner iframe's CSP is dynamically injected based on resource metadata -->
78
<title>MCP-UI Proxy</title>

examples/basic-server-react/mcp-app.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
<head>
44
<meta charset="UTF-8">
55
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<meta name="color-scheme" content="light dark">
67
<title>Get Time App</title>
78
<link rel="stylesheet" href="/src/global.css">
89
</head>

examples/basic-server-vanillajs/mcp-app.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
<head>
44
<meta charset="UTF-8">
55
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<meta name="color-scheme" content="light dark">
67
<title>Get Time App</title>
78
</head>
89
<body>

examples/budget-allocator-server/mcp-app.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
<head>
44
<meta charset="UTF-8">
55
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<meta name="color-scheme" content="light dark">
67
<title>Budget Allocator</title>
78
</head>
89
<body>

examples/cohort-heatmap-server/mcp-app.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
<head>
44
<meta charset="UTF-8">
55
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<meta name="color-scheme" content="light dark">
67
<title>Cohort Retention Heatmap</title>
78
<link rel="stylesheet" href="/src/global.css">
89
</head>

examples/customer-segmentation-server/mcp-app.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
<head>
44
<meta charset="UTF-8">
55
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<meta name="color-scheme" content="light dark">
67
<title>Customer Segmentation Explorer</title>
78
</head>
89
<body>

examples/customer-segmentation-server/src/mcp-app.css

Lines changed: 52 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,45 @@
1+
/* Default fallback values for host style variables */
12
:root {
2-
--color-bg: #ffffff;
3-
--color-text: #1f2937;
4-
--color-text-muted: #6b7280;
5-
--color-primary: #2563eb;
6-
--color-primary-hover: #1d4ed8;
7-
--color-card-bg: #f9fafb;
8-
--color-border: #e5e7eb;
9-
--color-enterprise: #1e40af;
10-
--color-midmarket: #0d9488;
11-
--color-smb: #059669;
12-
--color-startup: #6366f1;
13-
}
14-
15-
@media (prefers-color-scheme: dark) {
16-
:root {
17-
--color-bg: #111827;
18-
--color-text: #f9fafb;
19-
--color-text-muted: #9ca3af;
20-
--color-primary: #3b82f6;
21-
--color-primary-hover: #60a5fa;
22-
--color-card-bg: #1f2937;
23-
--color-border: #374151;
24-
--color-enterprise: #3b82f6;
25-
--color-midmarket: #14b8a6;
26-
--color-smb: #10b981;
27-
--color-startup: #818cf8;
28-
}
3+
color-scheme: light dark;
4+
5+
/* Background colors */
6+
--color-background-primary: light-dark(#ffffff, #111827);
7+
--color-background-secondary: light-dark(#f9fafb, #1f2937);
8+
--color-background-tertiary: light-dark(#f3f4f6, #374151);
9+
10+
/* Text colors */
11+
--color-text-primary: light-dark(#1f2937, #f9fafb);
12+
--color-text-secondary: light-dark(#6b7280, #9ca3af);
13+
--color-text-tertiary: light-dark(#9ca3af, #6b7280);
14+
15+
/* Border colors */
16+
--color-border-primary: light-dark(#e5e7eb, #374151);
17+
--color-border-secondary: light-dark(#d1d5db, #4b5563);
18+
19+
/* Accent colors */
20+
--color-accent-info: light-dark(#2563eb, #3b82f6);
21+
22+
/* Border radius */
23+
--border-radius-sm: 6px;
24+
--border-radius-md: 8px;
25+
26+
/* App-specific colors (not from host) */
27+
--color-enterprise: light-dark(#1e40af, #3b82f6);
28+
--color-midmarket: light-dark(#0d9488, #14b8a6);
29+
--color-smb: light-dark(#059669, #10b981);
30+
--color-startup: light-dark(#6366f1, #818cf8);
2931
}
3032

3133
html, body {
3234
margin: 0;
3335
padding: 0;
34-
background: var(--color-bg);
35-
color: var(--color-text);
36+
color: var(--color-text-primary);
3637
overflow: hidden;
3738
}
3839

3940
.main {
41+
border-radius: var(--border-radius-md);
42+
background: var(--color-background-primary);
4043
width: 100%;
4144
height: 600px;
4245
margin: 0 auto;
@@ -85,32 +88,32 @@ html, body {
8588
gap: 6px;
8689
font-size: 0.8125rem;
8790
font-weight: 500;
88-
color: var(--color-text-muted);
91+
color: var(--color-text-secondary);
8992
}
9093

9194
.select {
9295
padding: 4px 8px;
9396
font-size: 0.8125rem;
94-
border: 1px solid var(--color-border);
95-
border-radius: 4px;
96-
background: var(--color-card-bg);
97-
color: var(--color-text);
97+
border: 1px solid var(--color-border-primary);
98+
border-radius: var(--border-radius-sm);
99+
background: var(--color-background-secondary);
100+
color: var(--color-text-primary);
98101
cursor: pointer;
99102
}
100103

101104
.select:focus {
102-
outline: 2px solid var(--color-primary);
105+
outline: 2px solid var(--color-accent-info);
103106
outline-offset: 1px;
104107
}
105108

106109
/* Chart section - ~420px */
107110
.chart-section {
108111
flex: 1;
109112
min-height: 0;
110-
background: var(--color-card-bg);
111-
border-radius: 8px;
113+
background: var(--color-background-secondary);
114+
border-radius: var(--border-radius-md);
112115
padding: 8px;
113-
border: 1px solid var(--color-border);
116+
border: 1px solid var(--color-border-primary);
114117
}
115118

116119
.chart-container {
@@ -141,13 +144,13 @@ html, body {
141144
cursor: pointer;
142145
padding: 4px 10px;
143146
border-radius: 16px;
144-
border: 1px solid var(--color-border);
145-
background: var(--color-card-bg);
147+
border: 1px solid var(--color-border-primary);
148+
background: var(--color-background-secondary);
146149
transition: all 0.15s ease;
147150
}
148151

149152
.legend-item:hover {
150-
border-color: var(--color-text-muted);
153+
border-color: var(--color-text-secondary);
151154
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
152155
}
153156

@@ -171,7 +174,7 @@ html, body {
171174
}
172175

173176
.legend-count {
174-
color: var(--color-text-muted);
177+
color: var(--color-text-secondary);
175178
}
176179

177180
/* Detail section - ~44px */
@@ -186,15 +189,15 @@ html, body {
186189
justify-content: center;
187190
gap: 16px;
188191
height: 100%;
189-
background: var(--color-card-bg);
190-
border-radius: 6px;
192+
background: var(--color-background-secondary);
193+
border-radius: var(--border-radius-sm);
191194
padding: 0 12px;
192-
border: 1px solid var(--color-border);
195+
border: 1px solid var(--color-border-primary);
193196
font-size: 0.8125rem;
194197
}
195198

196199
.detail-placeholder {
197-
color: var(--color-text-muted);
200+
color: var(--color-text-secondary);
198201
}
199202

200203
.detail-name {
@@ -203,7 +206,7 @@ html, body {
203206

204207
.detail-segment {
205208
padding: 2px 8px;
206-
border-radius: 4px;
209+
border-radius: var(--border-radius-sm);
207210
font-size: 0.75rem;
208211
font-weight: 500;
209212
color: white;
@@ -215,10 +218,10 @@ html, body {
215218
.detail-segment.startup { background: var(--color-startup); }
216219

217220
.detail-metric {
218-
color: var(--color-text-muted);
221+
color: var(--color-text-secondary);
219222
}
220223

221224
.detail-metric strong {
222-
color: var(--color-text);
225+
color: var(--color-text-primary);
223226
font-weight: 600;
224227
}

examples/customer-segmentation-server/src/mcp-app.ts

Lines changed: 85 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
/**
22
* @file Customer Segmentation Explorer - interactive scatter/bubble visualization
33
*/
4-
import { App, PostMessageTransport } from "@modelcontextprotocol/ext-apps";
4+
import {
5+
App,
6+
PostMessageTransport,
7+
applyHostStyleVariables,
8+
applyDocumentTheme,
9+
} from "@modelcontextprotocol/ext-apps";
510
import { Chart, registerables } from "chart.js";
611
import "./global.css";
712
import "./mcp-app.css";
@@ -138,11 +143,40 @@ function buildDatasets(): Chart["data"]["datasets"] {
138143
});
139144
}
140145

146+
// Hidden element for resolving CSS color values (reused to avoid DOM thrashing)
147+
let colorResolver: HTMLDivElement | null = null;
148+
149+
// Resolve a CSS color value (handles light-dark() function)
150+
function resolveColor(cssValue: string, fallback: string): string {
151+
if (!cssValue) return fallback;
152+
// If it's a simple color value, return it directly
153+
if (!cssValue.includes("light-dark(")) return cssValue;
154+
// Create resolver element once and keep it hidden
155+
if (!colorResolver) {
156+
colorResolver = document.createElement("div");
157+
colorResolver.style.position = "absolute";
158+
colorResolver.style.visibility = "hidden";
159+
colorResolver.style.pointerEvents = "none";
160+
document.body.appendChild(colorResolver);
161+
}
162+
colorResolver.style.color = cssValue;
163+
return getComputedStyle(colorResolver).color || fallback;
164+
}
165+
166+
// Get colors from CSS variables
167+
function getChartColors(): { textColor: string; gridColor: string } {
168+
const style = getComputedStyle(document.documentElement);
169+
const rawTextColor = style.getPropertyValue("--color-text-secondary").trim();
170+
const rawGridColor = style.getPropertyValue("--color-border-primary").trim();
171+
return {
172+
textColor: resolveColor(rawTextColor, "#6b7280"),
173+
gridColor: resolveColor(rawGridColor, "#e5e7eb"),
174+
};
175+
}
176+
141177
// Initialize Chart.js
142178
function initChart(): Chart {
143-
const isDarkMode = window.matchMedia("(prefers-color-scheme: dark)").matches;
144-
const textColor = isDarkMode ? "#9ca3af" : "#6b7280";
145-
const gridColor = isDarkMode ? "#374151" : "#e5e7eb";
179+
const { textColor, gridColor } = getChartColors();
146180

147181
return new Chart(chartCanvas, {
148182
type: "bubble",
@@ -243,29 +277,34 @@ function initChart(): Chart {
243277
function updateChart(): void {
244278
if (!state.chart) return;
245279

246-
const isDarkMode = window.matchMedia("(prefers-color-scheme: dark)").matches;
247-
const textColor = isDarkMode ? "#9ca3af" : "#6b7280";
280+
const { textColor, gridColor } = getChartColors();
248281

249282
state.chart.data.datasets = buildDatasets();
250283

251284
// Update axis titles and formatters (using type assertions for Chart.js scale options)
252285
const scales = state.chart.options.scales as {
253286
x: {
254287
title: { text: string; color: string };
255-
ticks: { callback: (value: number) => string };
288+
ticks: { color: string; callback: (value: number) => string };
289+
grid: { color: string };
256290
};
257291
y: {
258292
title: { text: string; color: string };
259-
ticks: { callback: (value: number) => string };
293+
ticks: { color: string; callback: (value: number) => string };
294+
grid: { color: string };
260295
};
261296
};
262297

263298
scales.x.title.text = METRIC_LABELS[state.xAxis];
264299
scales.y.title.text = METRIC_LABELS[state.yAxis];
265300
scales.x.title.color = textColor;
266301
scales.y.title.color = textColor;
302+
scales.x.ticks.color = textColor;
303+
scales.y.ticks.color = textColor;
267304
scales.x.ticks.callback = (value: number) => formatValue(value, state.xAxis);
268305
scales.y.ticks.callback = (value: number) => formatValue(value, state.yAxis);
306+
scales.x.grid.color = gridColor;
307+
scales.y.grid.color = gridColor;
269308

270309
state.chart.update();
271310
}
@@ -388,20 +427,52 @@ document.addEventListener("click", (e) => {
388427
}
389428
});
390429

391-
// Handle theme changes
430+
// Handle system theme changes (fallback when host doesn't provide styles)
392431
window
393432
.matchMedia("(prefers-color-scheme: dark)")
394-
.addEventListener("change", () => {
395-
if (state.chart) {
396-
state.chart.destroy();
397-
state.chart = initChart();
433+
.addEventListener("change", (e) => {
434+
// Only apply if we haven't received host theme
435+
if (!app.getHostContext()?.theme) {
436+
applyDocumentTheme(e.matches ? "dark" : "light");
437+
if (state.chart) {
438+
state.chart.destroy();
439+
state.chart = initChart();
440+
}
398441
}
399442
});
400443

444+
// Apply initial theme based on system preference (before host context is available)
445+
const systemDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
446+
applyDocumentTheme(systemDark ? "dark" : "light");
447+
401448
// Register handlers and connect
402449
app.onerror = log.error;
403450

404-
app.connect(new PostMessageTransport(window.parent));
451+
// Handle host context changes (theme and styles from host)
452+
app.onhostcontextchanged = (params) => {
453+
if (params.theme) {
454+
applyDocumentTheme(params.theme);
455+
}
456+
if (params.styles?.variables) {
457+
applyHostStyleVariables(params.styles.variables);
458+
}
459+
// Recreate chart to pick up new colors
460+
if (state.chart && (params.theme || params.styles?.variables)) {
461+
state.chart.destroy();
462+
state.chart = initChart();
463+
}
464+
};
465+
466+
app.connect(new PostMessageTransport(window.parent)).then(() => {
467+
// Apply initial host context after connection
468+
const ctx = app.getHostContext();
469+
if (ctx?.theme) {
470+
applyDocumentTheme(ctx.theme);
471+
}
472+
if (ctx?.styles?.variables) {
473+
applyHostStyleVariables(ctx.styles.variables);
474+
}
475+
});
405476

406477
// Fetch data after connection
407478
setTimeout(fetchData, 100);

0 commit comments

Comments
 (0)