diff --git a/examples/shadertoy-server/mcp-app.html b/examples/shadertoy-server/mcp-app.html index d59774e..32c9d24 100644 --- a/examples/shadertoy-server/mcp-app.html +++ b/examples/shadertoy-server/mcp-app.html @@ -9,6 +9,14 @@
+
diff --git a/examples/shadertoy-server/package.json b/examples/shadertoy-server/package.json index c3f1866..62bdc1a 100644 --- a/examples/shadertoy-server/package.json +++ b/examples/shadertoy-server/package.json @@ -17,7 +17,9 @@ "build": "tsc --noEmit && cross-env INPUT=mcp-app.html vite build && tsc -p tsconfig.server.json && bun build server.ts --outdir dist --target node && bun build main.ts --outfile dist/index.js --target node --external \"./server.js\" --banner \"#!/usr/bin/env node\"", "watch": "cross-env INPUT=mcp-app.html vite build --watch", "serve": "bun --watch main.ts", + "serve:stdio": "bun main.ts --stdio", "start": "cross-env NODE_ENV=development npm run build && npm run serve", + "start:stdio": "cross-env NODE_ENV=development npm run build && npm run serve:stdio", "dev": "cross-env NODE_ENV=development concurrently 'npm run watch' 'npm run serve'", "prepublishOnly": "npm run build" }, diff --git a/examples/shadertoy-server/screenshot.png b/examples/shadertoy-server/screenshot.png index 3910d53..dee834a 100644 Binary files a/examples/shadertoy-server/screenshot.png and b/examples/shadertoy-server/screenshot.png differ diff --git a/examples/shadertoy-server/src/mcp-app.css b/examples/shadertoy-server/src/mcp-app.css index d939d88..bff4731 100644 --- a/examples/shadertoy-server/src/mcp-app.css +++ b/examples/shadertoy-server/src/mcp-app.css @@ -5,8 +5,61 @@ html, body { overflow: hidden; } +.main { + position: relative; + width: 100%; + height: 100%; +} + #canvas { width: 100%; height: 100%; display: block; } + +/* Fullscreen button */ +.fullscreen-btn { + position: absolute; + bottom: 16px; + right: 16px; + 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.7; + 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; +} + +/* Fullscreen mode */ +.main.fullscreen .fullscreen-btn .expand-icon { + display: none; +} + +.main.fullscreen .fullscreen-btn .collapse-icon { + display: block; +} diff --git a/examples/shadertoy-server/src/mcp-app.ts b/examples/shadertoy-server/src/mcp-app.ts index 4e2353b..2dd7d41 100644 --- a/examples/shadertoy-server/src/mcp-app.ts +++ b/examples/shadertoy-server/src/mcp-app.ts @@ -34,6 +34,13 @@ const log = { // Get element references const mainEl = document.querySelector(".main") as HTMLElement; const canvas = document.getElementById("canvas") as HTMLCanvasElement; +const fullscreenBtn = document.getElementById( + "fullscreen-btn", +) as HTMLButtonElement; + +// Display mode state +let currentDisplayMode: "inline" | "fullscreen" = "inline"; + // Resize canvas to fill viewport function resizeCanvas() { canvas.width = window.innerWidth; @@ -42,16 +49,55 @@ function resizeCanvas() { resizeCanvas(); window.addEventListener("resize", resizeCanvas); -// Handle host context changes (safe area insets) +// Handle host context changes (display mode) function handleHostContextChanged(ctx: McpUiHostContext) { - if (ctx.safeAreaInsets) { - mainEl.style.paddingTop = `${ctx.safeAreaInsets.top}px`; - mainEl.style.paddingRight = `${ctx.safeAreaInsets.right}px`; - mainEl.style.paddingBottom = `${ctx.safeAreaInsets.bottom}px`; - mainEl.style.paddingLeft = `${ctx.safeAreaInsets.left}px`; + // Note: We ignore safeAreaInsets to maximize shader display area + + // Show fullscreen button if available (only update if field is present) + if (ctx.availableDisplayModes !== undefined) { + if (ctx.availableDisplayModes.includes("fullscreen")) { + fullscreenBtn.classList.add("available"); + } else { + fullscreenBtn.classList.remove("available"); + } + } + + // Update display mode state and UI + if (ctx.displayMode) { + currentDisplayMode = ctx.displayMode as "inline" | "fullscreen"; + if (currentDisplayMode === "fullscreen") { + mainEl.classList.add("fullscreen"); + } else { + mainEl.classList.remove("fullscreen"); + } } } +// Handle Escape key to exit fullscreen +document.addEventListener("keydown", (e) => { + if (e.key === "Escape" && currentDisplayMode === "fullscreen") { + toggleFullscreen(); + } +}); + +// Toggle fullscreen mode +async function toggleFullscreen() { + const newMode = currentDisplayMode === "fullscreen" ? "inline" : "fullscreen"; + try { + const result = await app.requestDisplayMode({ mode: newMode }); + currentDisplayMode = result.mode as "inline" | "fullscreen"; + if (currentDisplayMode === "fullscreen") { + mainEl.classList.add("fullscreen"); + } else { + mainEl.classList.remove("fullscreen"); + } + } catch (err) { + log.error("Failed to change display mode:", err); + } +} + +fullscreenBtn.addEventListener("click", toggleFullscreen); + // ShaderToyLite instance let shaderToy: ShaderToyLiteInstance | null = null;