From 92b8b4663b3551ea33089cec429af6f784184d1a Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Thu, 12 Feb 2026 15:48:18 +0000 Subject: [PATCH 1/4] Add fullscreen button to threejs-server --- examples/threejs-server/src/global.css | 61 +++++++++++++++++- .../threejs-server/src/mcp-app-wrapper.tsx | 7 +++ examples/threejs-server/src/threejs-app.tsx | 62 ++++++++++++++++++- 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/examples/threejs-server/src/global.css b/examples/threejs-server/src/global.css index 55d812012..c969ccc41 100644 --- a/examples/threejs-server/src/global.css +++ b/examples/threejs-server/src/global.css @@ -6,7 +6,8 @@ box-sizing: border-box; } -html, body { +html, +body { font-family: var(--font-sans, system-ui, -apple-system, sans-serif); font-size: 1rem; margin: 0; @@ -45,3 +46,61 @@ html, body { font-family: var(--font-sans, system-ui); padding: 20px; } + +/* Fullscreen button */ +.fullscreen-btn { + position: absolute; + top: 10px; + right: 10px; + width: 40px; + height: 40px; + border: none; + border-radius: 8px; + background: rgba(0, 0, 0, 0.6); + color: white; + cursor: pointer; + display: none; /* Hidden by default, shown when fullscreen available */ + align-items: center; + justify-content: center; + transition: + background 0.2s, + opacity 0.2s; + opacity: 0; /* Initially invisible, shown on hover */ + z-index: 100; +} + +.fullscreen-btn:hover { + background: rgba(0, 0, 0, 0.8); + opacity: 1; +} + +.fullscreen-btn svg { + width: 20px; + height: 20px; +} + +.fullscreen-btn .collapse-icon { + display: none; +} + +.fullscreen-btn.available { + display: flex; +} + +/* Show button on container hover */ +.threejs-container:hover .fullscreen-btn.available { + opacity: 0.7; +} + +/* Fullscreen mode: swap icons and remove border radius */ +.threejs-container.fullscreen .fullscreen-btn .expand-icon { + display: none; +} + +.threejs-container.fullscreen .fullscreen-btn .collapse-icon { + display: block; +} + +.threejs-container.fullscreen canvas { + border-radius: 0 !important; +} diff --git a/examples/threejs-server/src/mcp-app-wrapper.tsx b/examples/threejs-server/src/mcp-app-wrapper.tsx index 4313fbbd5..3b2660156 100644 --- a/examples/threejs-server/src/mcp-app-wrapper.tsx +++ b/examples/threejs-server/src/mcp-app-wrapper.tsx @@ -37,6 +37,8 @@ export interface ViewProps> { openLink: App["openLink"]; /** Send log messages to the host */ sendLog: App["sendLog"]; + /** Request a display mode change (e.g. fullscreen) */ + requestDisplayMode: App["requestDisplayMode"]; } // ============================================================================= @@ -108,6 +110,10 @@ function McpAppWrapper() { (params) => app!.sendLog(params), [app], ); + const requestDisplayMode = useCallback( + (params) => app!.requestDisplayMode(params), + [app], + ); if (error) { return
Error: {error.message}
; @@ -127,6 +133,7 @@ function McpAppWrapper() { sendMessage={sendMessage} openLink={openLink} sendLog={sendLog} + requestDisplayMode={requestDisplayMode} /> ); } diff --git a/examples/threejs-server/src/threejs-app.tsx b/examples/threejs-server/src/threejs-app.tsx index 2b99266b0..b1c49074f 100644 --- a/examples/threejs-server/src/threejs-app.tsx +++ b/examples/threejs-server/src/threejs-app.tsx @@ -4,7 +4,7 @@ * Renders interactive 3D scenes using Three.js with streaming code preview. * Receives all MCP App props from the wrapper. */ -import { useState, useEffect, useRef } from "react"; +import { useState, useEffect, useRef, useCallback } from "react"; import * as THREE from "three"; import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js"; import { EffectComposer } from "three/examples/jsm/postprocessing/EffectComposer.js"; @@ -203,8 +203,12 @@ export default function ThreeJSApp({ sendMessage: _sendMessage, openLink: _openLink, sendLog: _sendLog, + requestDisplayMode, }: ThreeJSAppProps) { const [error, setError] = useState(null); + const [currentDisplayMode, setCurrentDisplayMode] = useState< + "inline" | "fullscreen" + >("inline"); const canvasRef = useRef(null); const containerRef = useRef(null); const animControllerRef = useRef { + if (hostContext?.displayMode) { + setCurrentDisplayMode(hostContext.displayMode as "inline" | "fullscreen"); + } + }, [hostContext?.displayMode]); + + const toggleFullscreen = useCallback(async () => { + const newMode = isFullscreen ? "inline" : "fullscreen"; + try { + const result = await requestDisplayMode({ mode: newMode }); + setCurrentDisplayMode(result.mode as "inline" | "fullscreen"); + } catch { + // ignore + } + }, [isFullscreen, requestDisplayMode]); + + // Escape key exits fullscreen + useEffect(() => { + const onKeyDown = (e: KeyboardEvent) => { + if (e.key === "Escape" && isFullscreen) toggleFullscreen(); + }; + document.addEventListener("keydown", onKeyDown); + return () => document.removeEventListener("keydown", onKeyDown); + }, [isFullscreen, toggleFullscreen]); + // Visibility-based pause/play useEffect(() => { if (!containerRef.current) return; @@ -269,7 +303,7 @@ export default function ThreeJSApp({ return (
{error &&
Error: {error}
} +
); } From 43da11c63440a5b065ba94ca42ab9079aa7dc883 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Fri, 13 Feb 2026 18:18:22 +0000 Subject: [PATCH 2/4] Simplify wrapper: pass app instance directly instead of useCallback wrappers --- .../threejs-server/src/mcp-app-wrapper.tsx | 42 ++----------------- examples/threejs-server/src/threejs-app.tsx | 11 ++--- 2 files changed, 7 insertions(+), 46 deletions(-) diff --git a/examples/threejs-server/src/mcp-app-wrapper.tsx b/examples/threejs-server/src/mcp-app-wrapper.tsx index 3b2660156..f9dd263e5 100644 --- a/examples/threejs-server/src/mcp-app-wrapper.tsx +++ b/examples/threejs-server/src/mcp-app-wrapper.tsx @@ -7,7 +7,7 @@ import type { App, McpUiHostContext } from "@modelcontextprotocol/ext-apps"; import { useApp, useHostStyles } from "@modelcontextprotocol/ext-apps/react"; import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; -import { StrictMode, useState, useCallback, useEffect } from "react"; +import { StrictMode, useState, useEffect } from "react"; import { createRoot } from "react-dom/client"; import ThreeJSApp from "./threejs-app.tsx"; import "./global.css"; @@ -21,6 +21,8 @@ import "./global.css"; * This interface can be reused for other views. */ export interface ViewProps> { + /** The connected MCP App instance */ + app: App; /** Complete tool input (after streaming finishes) */ toolInputs: TToolInput | null; /** Partial tool input (during streaming) */ @@ -29,16 +31,6 @@ export interface ViewProps> { toolResult: CallToolResult | null; /** Host context (theme, dimensions, locale, etc.) */ hostContext: McpUiHostContext | null; - /** Call a tool on the MCP server */ - callServerTool: App["callServerTool"]; - /** Send a message to the host's chat */ - sendMessage: App["sendMessage"]; - /** Request the host to open a URL */ - openLink: App["openLink"]; - /** Send log messages to the host */ - sendLog: App["sendLog"]; - /** Request a display mode change (e.g. fullscreen) */ - requestDisplayMode: App["requestDisplayMode"]; } // ============================================================================= @@ -93,28 +85,6 @@ function McpAppWrapper() { } }, [app]); - // Memoized callbacks that forward to app methods - const callServerTool = useCallback( - (params, options) => app!.callServerTool(params, options), - [app], - ); - const sendMessage = useCallback( - (params, options) => app!.sendMessage(params, options), - [app], - ); - const openLink = useCallback( - (params, options) => app!.openLink(params, options), - [app], - ); - const sendLog = useCallback( - (params) => app!.sendLog(params), - [app], - ); - const requestDisplayMode = useCallback( - (params) => app!.requestDisplayMode(params), - [app], - ); - if (error) { return
Error: {error.message}
; } @@ -125,15 +95,11 @@ function McpAppWrapper() { return ( ); } diff --git a/examples/threejs-server/src/threejs-app.tsx b/examples/threejs-server/src/threejs-app.tsx index b1c49074f..1799c862f 100644 --- a/examples/threejs-server/src/threejs-app.tsx +++ b/examples/threejs-server/src/threejs-app.tsx @@ -195,15 +195,10 @@ async function executeThreeCode( // ============================================================================= export default function ThreeJSApp({ + app, toolInputs, toolInputsPartial, - toolResult: _toolResult, hostContext, - callServerTool: _callServerTool, - sendMessage: _sendMessage, - openLink: _openLink, - sendLog: _sendLog, - requestDisplayMode, }: ThreeJSAppProps) { const [error, setError] = useState(null); const [currentDisplayMode, setCurrentDisplayMode] = useState< @@ -242,12 +237,12 @@ export default function ThreeJSApp({ const toggleFullscreen = useCallback(async () => { const newMode = isFullscreen ? "inline" : "fullscreen"; try { - const result = await requestDisplayMode({ mode: newMode }); + const result = await app.requestDisplayMode({ mode: newMode }); setCurrentDisplayMode(result.mode as "inline" | "fullscreen"); } catch { // ignore } - }, [isFullscreen, requestDisplayMode]); + }, [isFullscreen, app]); // Escape key exits fullscreen useEffect(() => { From 6feccd5a788a38f0ab4799b47a6ae740ba19e2f5 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Fri, 13 Feb 2026 21:05:52 +0000 Subject: [PATCH 3/4] Update examples/threejs-server/src/threejs-app.tsx Co-authored-by: Jonathan Hefner --- examples/threejs-server/src/threejs-app.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/threejs-server/src/threejs-app.tsx b/examples/threejs-server/src/threejs-app.tsx index 1799c862f..df91ab69a 100644 --- a/examples/threejs-server/src/threejs-app.tsx +++ b/examples/threejs-server/src/threejs-app.tsx @@ -229,8 +229,8 @@ export default function ThreeJSApp({ // Sync display mode from host context useEffect(() => { - if (hostContext?.displayMode) { - setCurrentDisplayMode(hostContext.displayMode as "inline" | "fullscreen"); + if (hostContext?.displayMode === "inline" || hostContext?.displayMode === "fullscreen") { + setCurrentDisplayMode(hostContext.displayMode); } }, [hostContext?.displayMode]); From 899d9e2d1b5df4b47168e0a903134d3a3bf13847 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Fri, 13 Feb 2026 21:06:10 +0000 Subject: [PATCH 4/4] Update examples/threejs-server/src/threejs-app.tsx Co-authored-by: Jonathan Hefner --- examples/threejs-server/src/threejs-app.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/examples/threejs-server/src/threejs-app.tsx b/examples/threejs-server/src/threejs-app.tsx index df91ab69a..b640d0a6e 100644 --- a/examples/threejs-server/src/threejs-app.tsx +++ b/examples/threejs-server/src/threejs-app.tsx @@ -238,7 +238,9 @@ export default function ThreeJSApp({ const newMode = isFullscreen ? "inline" : "fullscreen"; try { const result = await app.requestDisplayMode({ mode: newMode }); - setCurrentDisplayMode(result.mode as "inline" | "fullscreen"); + if (result.mode === newMode) { + setCurrentDisplayMode(result.mode); + } } catch { // ignore }