Skip to content

Commit ec3409a

Browse files
Add shadertoy-server example for WebGL shader rendering (#226)
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 <[email protected]>
1 parent b273279 commit ec3409a

File tree

17 files changed

+1641
-1901
lines changed

17 files changed

+1641
-1901
lines changed

.prettierignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ examples/basic-host/**/*.ts
22
examples/basic-host/**/*.tsx
33
examples/basic-server-*/**/*.ts
44
examples/basic-server-*/**/*.tsx
5+
**/vendor/**
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
# Example: ShaderToy Server
2+
3+
A demo MCP App that renders [ShaderToy](https://www.shadertoy.com/)-compatible GLSL fragment shaders in real-time using WebGL 2.0 and [ShaderToyLite.js](https://github.com/nickoala/ShaderToyLite).
4+
5+
<table>
6+
<tr>
7+
<td><a href="https://modelcontextprotocol.github.io/ext-apps/screenshots/shadertoy-server/01-gradient.png"><img src="https://modelcontextprotocol.github.io/ext-apps/screenshots/shadertoy-server/01-gradient.png" alt="Gradient" width="100%"></a></td>
8+
<td><a href="https://modelcontextprotocol.github.io/ext-apps/screenshots/shadertoy-server/02-kaleidoscope.png"><img src="https://modelcontextprotocol.github.io/ext-apps/screenshots/shadertoy-server/02-kaleidoscope.png" alt="Kaleidoscope" width="100%"></a></td>
9+
<td><a href="https://modelcontextprotocol.github.io/ext-apps/screenshots/shadertoy-server/03-fractal.png"><img src="https://modelcontextprotocol.github.io/ext-apps/screenshots/shadertoy-server/03-fractal.png" alt="Kaleidoscope" width="100%"></a></td>
10+
</tr>
11+
</table>
12+
13+
## Features
14+
15+
- **Real-time Rendering**: Renders GLSL shaders using WebGL 2.0
16+
- **ShaderToy Compatibility**: Uses the standard `mainImage(out vec4 fragColor, in vec2 fragCoord)` entry point
17+
- **Multi-pass Rendering**: Supports buffers A-D for feedback effects, blur chains, and simulations
18+
- **Standard Uniforms**: iResolution, iTime, iTimeDelta, iFrame, iMouse, iDate, iChannel0-3
19+
20+
## Running
21+
22+
1. Install dependencies:
23+
24+
```bash
25+
npm install
26+
```
27+
28+
2. Build and start the server:
29+
30+
```bash
31+
npm run start:http # for Streamable HTTP transport
32+
# OR
33+
npm run start:stdio # for stdio transport
34+
```
35+
36+
3. View using the [`basic-host`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-host) example or another MCP Apps-compatible host.
37+
38+
### Tool Input Examples
39+
40+
**Gradient with Time:**
41+
42+
```glsl
43+
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
44+
vec2 uv = fragCoord / iResolution.xy;
45+
fragColor = vec4(uv, 0.5 + 0.5*sin(iTime), 1.0);
46+
}
47+
```
48+
49+
_Tool input:_
50+
51+
```json
52+
{
53+
"fragmentShader": "void mainImage(out vec4 fragColor, in vec2 fragCoord) {\n vec2 uv = fragCoord / iResolution.xy;\n fragColor = vec4(uv, 0.5 + 0.5*sin(iTime), 1.0);\n}"
54+
}
55+
```
56+
57+
**Kaleidoscope**:
58+
59+
```glsl
60+
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
61+
vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y;
62+
float segments = 6.0;
63+
float zoom = 1.0 + 0.3 * sin(iTime * 0.2);
64+
float angle = atan(uv.y, uv.x) + iTime * 0.3;
65+
float r = length(uv) * zoom;
66+
angle = mod(angle, 6.28 / segments);
67+
angle = abs(angle - 3.14 / segments);
68+
vec2 p = vec2(cos(angle), sin(angle)) * r;
69+
p += iTime * 0.1;
70+
float v = sin(p.x * 10.0) * sin(p.y * 10.0);
71+
v += sin(length(p) * 15.0 - iTime * 2.0);
72+
v += sin(p.x * 5.0 + p.y * 7.0 + iTime);
73+
vec3 col = 0.5 + 0.5 * cos(v * 2.0 + vec3(0.0, 2.0, 4.0) + iTime);
74+
fragColor = vec4(col, 1.0);
75+
}
76+
```
77+
78+
_Tool input:_
79+
80+
```json
81+
{
82+
"fragmentShader": "void mainImage(out vec4 fragColor, in vec2 fragCoord) {\n vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y;\n float segments = 6.0;\n float zoom = 1.0 + 0.3 * sin(iTime * 0.2);\n float angle = atan(uv.y, uv.x) + iTime * 0.3;\n float r = length(uv) * zoom;\n angle = mod(angle, 6.28 / segments);\n angle = abs(angle - 3.14 / segments);\n vec2 p = vec2(cos(angle), sin(angle)) * r;\n p += iTime * 0.1;\n float v = sin(p.x * 10.0) * sin(p.y * 10.0);\n v += sin(length(p) * 15.0 - iTime * 2.0);\n v += sin(p.x * 5.0 + p.y * 7.0 + iTime);\n vec3 col = 0.5 + 0.5 * cos(v * 2.0 + vec3(0.0, 2.0, 4.0) + iTime);\n fragColor = vec4(col, 1.0);\n}"
83+
}
84+
```
85+
86+
**Interactive Julia Set** (mouse controls the fractal's c parameter):
87+
88+
```glsl
89+
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
90+
vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y * 2.5;
91+
vec2 mouse = (iMouse.xy / iResolution.xy - 0.5) * 2.0;
92+
vec2 c = mouse;
93+
vec2 z = uv;
94+
float iter = 0.0;
95+
for (int i = 0; i < 100; i++) {
96+
z = vec2(z.x * z.x - z.y * z.y, 2.0 * z.x * z.y) + c;
97+
if (dot(z, z) > 4.0) break;
98+
iter++;
99+
}
100+
float t = iter / 100.0;
101+
vec3 col = 0.5 + 0.5 * cos(3.0 + t * 6.28 * 2.0 + vec3(0.0, 0.6, 1.0));
102+
if (iter == 100.0) col = vec3(0.0);
103+
fragColor = vec4(col, 1.0);
104+
}
105+
```
106+
107+
_Tool input:_
108+
109+
```json
110+
{
111+
"fragmentShader": "void mainImage(out vec4 fragColor, in vec2 fragCoord) {\n vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y * 2.5;\n vec2 mouse = (iMouse.xy / iResolution.xy - 0.5) * 2.0;\n vec2 c = mouse;\n vec2 z = uv;\n float iter = 0.0;\n for (int i = 0; i < 100; i++) {\n z = vec2(z.x * z.x - z.y * z.y, 2.0 * z.x * z.y) + c;\n if (dot(z, z) > 4.0) break;\n iter++;\n }\n float t = iter / 100.0;\n vec3 col = 0.5 + 0.5 * cos(3.0 + t * 6.28 * 2.0 + vec3(0.0, 0.6, 1.0));\n if (iter == 100.0) col = vec3(0.0);\n fragColor = vec4(col, 1.0);\n}"
112+
}
113+
```
114+
115+
## Architecture
116+
117+
### Server (`server.ts`)
118+
119+
Exposes a single `render-shadertoy` tool that accepts:
120+
121+
- `fragmentShader`: Main Image shader code (required)
122+
- `common`: Shared code across all shaders (optional)
123+
- `bufferA`: Buffer A shader, accessible as iChannel0 (optional)
124+
- `bufferB`: Buffer B shader, accessible as iChannel1 (optional)
125+
- `bufferC`: Buffer C shader, accessible as iChannel2 (optional)
126+
- `bufferD`: Buffer D shader, accessible as iChannel3 (optional)
127+
128+
### App (`src/mcp-app.ts`)
129+
130+
- Receives shader code via `ontoolinput` handler
131+
- Uses ShaderToyLite.js for WebGL rendering
132+
- Displays compilation errors in an overlay
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<meta name="color-scheme" content="light dark">
7+
<title>ShaderToy Renderer</title>
8+
</head>
9+
<body>
10+
<main class="main">
11+
<canvas id="canvas"></canvas>
12+
</main>
13+
<script type="module" src="/src/mcp-app.ts"></script>
14+
</body>
15+
</html>
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
{
2+
"name": "@modelcontextprotocol/server-shadertoy",
3+
"version": "0.1.0",
4+
"type": "module",
5+
"description": "MCP App Server example for rendering ShaderToy-compatible GLSL shaders",
6+
"repository": {
7+
"type": "git",
8+
"url": "https://github.com/modelcontextprotocol/ext-apps",
9+
"directory": "examples/shadertoy-server"
10+
},
11+
"license": "MIT",
12+
"main": "server.ts",
13+
"files": [
14+
"server.ts",
15+
"server-utils.ts",
16+
"dist"
17+
],
18+
"scripts": {
19+
"build": "tsc --noEmit && cross-env INPUT=mcp-app.html vite build",
20+
"watch": "cross-env INPUT=mcp-app.html vite build --watch",
21+
"serve": "bun server.ts",
22+
"start": "cross-env NODE_ENV=development npm run build && npm run serve",
23+
"dev": "cross-env NODE_ENV=development concurrently 'npm run watch' 'npm run serve'",
24+
"prepublishOnly": "npm run build"
25+
},
26+
"dependencies": {
27+
"@modelcontextprotocol/ext-apps": "^0.3.1",
28+
"@modelcontextprotocol/sdk": "^1.24.0",
29+
"zod": "^4.1.13"
30+
},
31+
"devDependencies": {
32+
"@types/cors": "^2.8.19",
33+
"@types/express": "^5.0.0",
34+
"@types/node": "^22.0.0",
35+
"concurrently": "^9.2.1",
36+
"cors": "^2.8.5",
37+
"cross-env": "^10.1.0",
38+
"express": "^5.1.0",
39+
"typescript": "^5.9.3",
40+
"vite": "^6.0.0",
41+
"vite-plugin-singlefile": "^2.3.0"
42+
}
43+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/**
2+
* Shared utilities for running MCP servers with Streamable HTTP transport.
3+
*/
4+
5+
import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
6+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
7+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
8+
import cors from "cors";
9+
import type { Request, Response } from "express";
10+
11+
export interface ServerOptions {
12+
port: number;
13+
name?: string;
14+
}
15+
16+
/**
17+
* Starts an MCP server with Streamable HTTP transport in stateless mode.
18+
*
19+
* @param createServer - Factory function that creates a new McpServer instance per request.
20+
* @param options - Server configuration options.
21+
*/
22+
export async function startServer(
23+
createServer: () => McpServer,
24+
options: ServerOptions,
25+
): Promise<void> {
26+
const { port, name = "MCP Server" } = options;
27+
28+
const app = createMcpExpressApp({ host: "0.0.0.0" });
29+
app.use(cors());
30+
31+
app.all("/mcp", async (req: Request, res: Response) => {
32+
const server = createServer();
33+
const transport = new StreamableHTTPServerTransport({
34+
sessionIdGenerator: undefined,
35+
});
36+
37+
res.on("close", () => {
38+
transport.close().catch(() => {});
39+
server.close().catch(() => {});
40+
});
41+
42+
try {
43+
await server.connect(transport);
44+
await transport.handleRequest(req, res, req.body);
45+
} catch (error) {
46+
console.error("MCP error:", error);
47+
if (!res.headersSent) {
48+
res.status(500).json({
49+
jsonrpc: "2.0",
50+
error: { code: -32603, message: "Internal server error" },
51+
id: null,
52+
});
53+
}
54+
}
55+
});
56+
57+
const httpServer = app.listen(port, (err) => {
58+
if (err) {
59+
console.error("Failed to start server:", err);
60+
process.exit(1);
61+
}
62+
console.log(`${name} listening on http://localhost:${port}/mcp`);
63+
});
64+
65+
const shutdown = () => {
66+
console.log("\nShutting down...");
67+
httpServer.close(() => process.exit(0));
68+
};
69+
70+
process.on("SIGINT", shutdown);
71+
process.on("SIGTERM", shutdown);
72+
}

0 commit comments

Comments
 (0)