Introduces a new MCP App example that renders ShaderToy-compatible GLSL fragment shaders in real-time using WebGL 2.0 and ShaderToyLite.js. Features: - Standard ShaderToy uniforms (`iTime`, `iResolution`, `iMouse`, etc.) - Multi-pass rendering with buffers A-D for feedback effects - Shader compilation error overlay Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
126 lines
3.2 KiB
TypeScript
126 lines
3.2 KiB
TypeScript
/**
|
|
* ShaderToy renderer MCP App using ShaderToyLite.js
|
|
*/
|
|
import { App, type McpUiHostContext } from "@modelcontextprotocol/ext-apps";
|
|
import "./global.css";
|
|
import "./mcp-app.css";
|
|
import ShaderToyLite, {
|
|
type ShaderToyLiteInstance,
|
|
} from "./vendor/ShaderToyLite.js";
|
|
|
|
interface ShaderInput {
|
|
fragmentShader: string;
|
|
common?: string;
|
|
bufferA?: string;
|
|
bufferB?: string;
|
|
bufferC?: string;
|
|
bufferD?: string;
|
|
}
|
|
|
|
function isShaderInput(value: unknown): value is ShaderInput {
|
|
return (
|
|
typeof value === "object" &&
|
|
value !== null &&
|
|
typeof (value as Record<string, unknown>).fragmentShader === "string"
|
|
);
|
|
}
|
|
|
|
const log = {
|
|
info: console.log.bind(console, "[APP]"),
|
|
warn: console.warn.bind(console, "[APP]"),
|
|
error: console.error.bind(console, "[APP]"),
|
|
};
|
|
|
|
// Get element references
|
|
const mainEl = document.querySelector(".main") as HTMLElement;
|
|
const canvas = document.getElementById("canvas") as HTMLCanvasElement;
|
|
// Resize canvas to fill viewport
|
|
function resizeCanvas() {
|
|
canvas.width = window.innerWidth;
|
|
canvas.height = window.innerHeight;
|
|
}
|
|
resizeCanvas();
|
|
window.addEventListener("resize", resizeCanvas);
|
|
|
|
// Handle host context changes (safe area insets)
|
|
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`;
|
|
}
|
|
}
|
|
|
|
// ShaderToyLite instance
|
|
let shaderToy: ShaderToyLiteInstance | null = null;
|
|
|
|
// Create app instance
|
|
const app = new App({ name: "ShaderToy Renderer", version: "1.0.0" });
|
|
|
|
app.onteardown = async () => {
|
|
log.info("App is being torn down");
|
|
if (shaderToy) {
|
|
shaderToy.pause();
|
|
}
|
|
return {};
|
|
};
|
|
|
|
app.ontoolinput = (params) => {
|
|
log.info("Received shader input");
|
|
|
|
if (!isShaderInput(params.arguments)) {
|
|
log.error("Invalid tool input");
|
|
return;
|
|
}
|
|
|
|
const { fragmentShader, common, bufferA, bufferB, bufferC, bufferD } =
|
|
params.arguments;
|
|
|
|
// Initialize ShaderToyLite if needed
|
|
if (!shaderToy) {
|
|
shaderToy = new ShaderToyLite("canvas");
|
|
}
|
|
|
|
// Set common code (shared across all shaders)
|
|
shaderToy.setCommon(common || "");
|
|
|
|
// Set buffer shaders with self-feedback
|
|
if (bufferA) {
|
|
shaderToy.setBufferA({ source: bufferA, iChannel0: "A" });
|
|
}
|
|
if (bufferB) {
|
|
shaderToy.setBufferB({ source: bufferB, iChannel1: "B" });
|
|
}
|
|
if (bufferC) {
|
|
shaderToy.setBufferC({ source: bufferC, iChannel2: "C" });
|
|
}
|
|
if (bufferD) {
|
|
shaderToy.setBufferD({ source: bufferD, iChannel3: "D" });
|
|
}
|
|
|
|
// Set main Image shader with buffer inputs
|
|
shaderToy.setImage({
|
|
source: fragmentShader,
|
|
iChannel0: bufferA ? "A" : undefined,
|
|
iChannel1: bufferB ? "B" : undefined,
|
|
iChannel2: bufferC ? "C" : undefined,
|
|
iChannel3: bufferD ? "D" : undefined,
|
|
});
|
|
|
|
shaderToy.play();
|
|
log.info("Setup complete");
|
|
};
|
|
|
|
app.onerror = log.error;
|
|
|
|
app.onhostcontextchanged = handleHostContextChanged;
|
|
|
|
// Connect to host
|
|
app.connect().then(() => {
|
|
log.info("Connected to host");
|
|
const ctx = app.getHostContext();
|
|
if (ctx) {
|
|
handleHostContextChanged(ctx);
|
|
}
|
|
});
|