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;