Skip to content

Commit e06f2bd

Browse files
authored
Merge pull request #195 from modelcontextprotocol/martinalong/mcp-apps/fullscreen
[MCP Apps] Add appCapabilities.availableDisplayModes
2 parents dc226be + 5b6ac19 commit e06f2bd

7 files changed

Lines changed: 237 additions & 57 deletions

File tree

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@
1010
<main class="main">
1111
<header class="header">
1212
<h1 class="title">Customer Segmentation</h1>
13+
<button id="fullscreen-btn" class="fullscreen-btn" title="Toggle fullscreen">
14+
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
15+
<path d="M8.22461 11.082C8.41868 10.9539 8.68265 10.9756 8.85352 11.1465C9.02437 11.3173 9.04613 11.5813 8.91797 11.7754L8.85352 11.8535L4.70703 16H7.5C7.77614 16 8 16.2239 8 16.5C8 16.7761 7.77614 17 7.5 17H3.5C3.46695 17 3.43389 16.9967 3.40137 16.9902H3.39941C3.33993 16.9781 3.2851 16.9541 3.23535 16.9229C3.22314 16.9152 3.21185 16.9063 3.2002 16.8975C3.18573 16.8865 3.17138 16.8757 3.1582 16.8633C3.15452 16.8598 3.15008 16.8571 3.14648 16.8535C3.14173 16.8488 3.13832 16.8428 3.13379 16.8379C3.12089 16.8239 3.10891 16.8093 3.09766 16.7939C3.08941 16.7827 3.0814 16.7715 3.07422 16.7598C3.0639 16.7429 3.05517 16.7252 3.04688 16.707C3.04311 16.6988 3.03845 16.691 3.03516 16.6826C3.0248 16.6562 3.01653 16.6289 3.01074 16.6006C3.00395 16.5674 3 16.5337 3 16.5V12.5C3 12.2239 3.22386 12 3.5 12C3.77614 12 4 12.2239 4 12.5V15.293L8.14648 11.1465L8.22461 11.082ZM16.5 3C16.5374 3 16.5747 3.00436 16.6113 3.0127C16.6347 3.01803 16.6574 3.02559 16.6797 3.03418C16.687 3.03701 16.6939 3.04076 16.7012 3.04395C16.7213 3.05283 16.7409 3.06272 16.7598 3.07422C16.7675 3.07892 16.7757 3.08274 16.7832 3.08789C16.8082 3.10508 16.8317 3.12471 16.8535 3.14648L16.918 3.22461C16.9289 3.24116 16.9356 3.25988 16.9443 3.27734C16.95 3.28857 16.9572 3.29894 16.9619 3.31055C16.9868 3.37121 17 3.43545 17 3.5V7.5C17 7.77614 16.7761 8 16.5 8C16.2239 8 16 7.77614 16 7.5V4.70703L11.8535 8.85352C11.6583 9.04878 11.3417 9.04878 11.1465 8.85352C10.9512 8.65825 10.9512 8.34175 11.1465 8.14648L15.293 4H12.5C12.2239 4 12 3.77614 12 3.5C12 3.22386 12.2239 3 12.5 3H16.5Z" fill="currentColor"/>
16+
</svg>
17+
</button>
1318
</header>
1419

1520
<section class="controls-section">

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

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22
:root {
33
color-scheme: light dark;
44

5+
/* Safe area insets (set by JS from host context) */
6+
--safe-area-inset-top: 0px;
7+
--safe-area-inset-right: 0px;
8+
--safe-area-inset-bottom: 0px;
9+
--safe-area-inset-left: 0px;
10+
511
/* Font families */
612
--font-sans: system-ui, -apple-system, sans-serif;
713

@@ -61,6 +67,17 @@ html, body {
6167
overflow: hidden;
6268
}
6369

70+
/* Fullscreen mode - no border radius, fill container with safe area padding */
71+
[data-display-mode="fullscreen"] .main {
72+
border-radius: 0;
73+
height: 100vh;
74+
max-height: 100vh;
75+
padding-top: var(--safe-area-inset-top);
76+
padding-right: var(--safe-area-inset-right);
77+
padding-bottom: var(--safe-area-inset-bottom);
78+
padding-left: var(--safe-area-inset-left);
79+
}
80+
6481
/* Header - ~40px */
6582
.header {
6683
display: flex;
@@ -78,6 +95,42 @@ html, body {
7895
flex-shrink: 0;
7996
}
8097

98+
.fullscreen-btn {
99+
display: flex;
100+
align-items: center;
101+
justify-content: center;
102+
width: 28px;
103+
height: 28px;
104+
padding: 0;
105+
border: var(--border-width-regular) solid var(--color-border-primary);
106+
border-radius: var(--border-radius-sm);
107+
background: var(--color-background-secondary);
108+
color: var(--color-text-secondary);
109+
cursor: pointer;
110+
transition: all 0.15s ease;
111+
}
112+
113+
.fullscreen-btn:hover {
114+
background: var(--color-background-tertiary);
115+
color: var(--color-text-primary);
116+
box-shadow: var(--shadow-sm);
117+
}
118+
119+
.fullscreen-btn:focus {
120+
outline: 2px solid var(--color-ring-info);
121+
outline-offset: 1px;
122+
}
123+
124+
.fullscreen-btn svg {
125+
width: 16px;
126+
height: 16px;
127+
}
128+
129+
/* Hide fullscreen button when already in fullscreen mode */
130+
[data-display-mode="fullscreen"] .fullscreen-btn {
131+
display: none;
132+
}
133+
81134
.header-controls {
82135
display: flex;
83136
align-items: center;

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

Lines changed: 82 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,30 @@ const log = {
2121
error: console.error.bind(console, "[APP]"),
2222
};
2323

24+
/**
25+
* Apply safe area insets as CSS custom properties on the document root.
26+
*/
27+
function applySafeAreaInsets(insets: {
28+
top?: number;
29+
right?: number;
30+
bottom?: number;
31+
left?: number;
32+
}) {
33+
const root = document.documentElement;
34+
if (insets.top !== undefined) {
35+
root.style.setProperty("--safe-area-inset-top", `${insets.top}px`);
36+
}
37+
if (insets.right !== undefined) {
38+
root.style.setProperty("--safe-area-inset-right", `${insets.right}px`);
39+
}
40+
if (insets.bottom !== undefined) {
41+
root.style.setProperty("--safe-area-inset-bottom", `${insets.bottom}px`);
42+
}
43+
if (insets.left !== undefined) {
44+
root.style.setProperty("--safe-area-inset-left", `${insets.left}px`);
45+
}
46+
}
47+
2448
// DOM element references
2549
const xAxisSelect = document.getElementById("x-axis") as HTMLSelectElement;
2650
const yAxisSelect = document.getElementById("y-axis") as HTMLSelectElement;
@@ -32,6 +56,9 @@ const chartCanvas = document.getElementById(
3256
) as HTMLCanvasElement;
3357
const legendContainer = document.getElementById("legend")!;
3458
const detailPanel = document.getElementById("detail-panel")!;
59+
const fullscreenBtn = document.getElementById(
60+
"fullscreen-btn",
61+
) as HTMLButtonElement;
3562

3663
// App state
3764
interface AppState {
@@ -364,8 +391,11 @@ function resetDetailPanel(): void {
364391
'<span class="detail-placeholder">Hover over a point to see details</span>';
365392
}
366393

367-
// Create app instance
368-
const app = new App({ name: "Customer Segmentation", version: "1.0.0" });
394+
// Create app instance with fullscreen support
395+
const app = new App(
396+
{ name: "Customer Segmentation", version: "1.0.0" },
397+
{ availableDisplayModes: ["inline", "fullscreen"] },
398+
);
369399

370400
// Fetch data from server
371401
async function fetchData(): Promise<void> {
@@ -419,6 +449,16 @@ sizeMetricSelect.addEventListener("change", () => {
419449
updateChart();
420450
});
421451

452+
// Fullscreen toggle
453+
fullscreenBtn.addEventListener("click", async () => {
454+
try {
455+
const result = await app.requestDisplayMode({ mode: "fullscreen" });
456+
log.info("Display mode changed to:", result.mode);
457+
} catch (error) {
458+
log.error("Failed to change display mode:", error);
459+
}
460+
});
461+
422462
// Clear selection when clicking outside chart
423463
document.addEventListener("click", (e) => {
424464
if (!(e.target as HTMLElement).closest(".chart-section")) {
@@ -459,6 +499,33 @@ app.onhostcontextchanged = (params) => {
459499
if (params.styles?.css?.fonts) {
460500
applyHostFonts(params.styles.css.fonts);
461501
}
502+
if (params.displayMode) {
503+
document.documentElement.setAttribute(
504+
"data-display-mode",
505+
params.displayMode,
506+
);
507+
}
508+
if (params.safeAreaInsets) {
509+
applySafeAreaInsets(params.safeAreaInsets);
510+
}
511+
// Update container height based on containerDimensions from host
512+
// This ensures the chart resizes correctly during transitions
513+
if (params.containerDimensions) {
514+
const mainEl = document.querySelector(".main") as HTMLElement | null;
515+
if (mainEl) {
516+
if ("height" in params.containerDimensions) {
517+
// If height is fixed, take up all the height
518+
mainEl.style.height = "100vh";
519+
} else if ("maxHeight" in params.containerDimensions) {
520+
// If height is variable, let the rest of the css determine the height
521+
mainEl.style.height = "";
522+
}
523+
// Resize chart after container dimensions change
524+
if (state.chart) {
525+
state.chart.resize();
526+
}
527+
}
528+
}
462529
// Recreate chart to pick up new colors
463530
if (state.chart && (params.theme || params.styles?.variables)) {
464531
state.chart.destroy();
@@ -478,6 +545,19 @@ app.connect().then(() => {
478545
if (ctx?.styles?.css?.fonts) {
479546
applyHostFonts(ctx.styles.css.fonts);
480547
}
548+
if (ctx?.displayMode) {
549+
document.documentElement.setAttribute("data-display-mode", ctx.displayMode);
550+
}
551+
if (ctx?.safeAreaInsets) {
552+
applySafeAreaInsets(ctx.safeAreaInsets);
553+
}
554+
// Apply initial container dimensions
555+
if (ctx?.containerDimensions) {
556+
const mainEl = document.querySelector(".main") as HTMLElement | null;
557+
if (mainEl && "height" in ctx.containerDimensions) {
558+
mainEl.style.height = `${ctx.containerDimensions.height}px`;
559+
}
560+
}
481561
});
482562

483563
// Fetch data after connection

package-lock.json

Lines changed: 1 addition & 50 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)