Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ examples/basic-host/**/*.ts
examples/basic-host/**/*.tsx
examples/basic-server-*/**/*.ts
examples/basic-server-*/**/*.tsx
**/vendor/**
132 changes: 132 additions & 0 deletions examples/shadertoy-server/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
# Example: ShaderToy Server

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).

<table>
<tr>
<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>
<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>
<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>
</tr>
</table>

## Features

- **Real-time Rendering**: Renders GLSL shaders using WebGL 2.0
- **ShaderToy Compatibility**: Uses the standard `mainImage(out vec4 fragColor, in vec2 fragCoord)` entry point
- **Multi-pass Rendering**: Supports buffers A-D for feedback effects, blur chains, and simulations
- **Standard Uniforms**: iResolution, iTime, iTimeDelta, iFrame, iMouse, iDate, iChannel0-3

## Running

1. Install dependencies:

```bash
npm install
```

2. Build and start the server:

```bash
npm run start:http # for Streamable HTTP transport
# OR
npm run start:stdio # for stdio transport
```

3. View using the [`basic-host`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-host) example or another MCP Apps-compatible host.

### Tool Input Examples

**Gradient with Time:**

```glsl
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 uv = fragCoord / iResolution.xy;
fragColor = vec4(uv, 0.5 + 0.5*sin(iTime), 1.0);
}
```

_Tool input:_

```json
{
"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}"
}
```

**Kaleidoscope**:

```glsl
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y;
float segments = 6.0;
float zoom = 1.0 + 0.3 * sin(iTime * 0.2);
float angle = atan(uv.y, uv.x) + iTime * 0.3;
float r = length(uv) * zoom;
angle = mod(angle, 6.28 / segments);
angle = abs(angle - 3.14 / segments);
vec2 p = vec2(cos(angle), sin(angle)) * r;
p += iTime * 0.1;
float v = sin(p.x * 10.0) * sin(p.y * 10.0);
v += sin(length(p) * 15.0 - iTime * 2.0);
v += sin(p.x * 5.0 + p.y * 7.0 + iTime);
vec3 col = 0.5 + 0.5 * cos(v * 2.0 + vec3(0.0, 2.0, 4.0) + iTime);
fragColor = vec4(col, 1.0);
}
```

_Tool input:_

```json
{
"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}"
}
```

**Interactive Julia Set** (mouse controls the fractal's c parameter):

```glsl
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y * 2.5;
vec2 mouse = (iMouse.xy / iResolution.xy - 0.5) * 2.0;
vec2 c = mouse;
vec2 z = uv;
float iter = 0.0;
for (int i = 0; i < 100; i++) {
z = vec2(z.x * z.x - z.y * z.y, 2.0 * z.x * z.y) + c;
if (dot(z, z) > 4.0) break;
iter++;
}
float t = iter / 100.0;
vec3 col = 0.5 + 0.5 * cos(3.0 + t * 6.28 * 2.0 + vec3(0.0, 0.6, 1.0));
if (iter == 100.0) col = vec3(0.0);
fragColor = vec4(col, 1.0);
}
```

_Tool input:_

```json
{
"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}"
}
```

## Architecture

### Server (`server.ts`)

Exposes a single `render-shadertoy` tool that accepts:

- `fragmentShader`: Main Image shader code (required)
- `common`: Shared code across all shaders (optional)
- `bufferA`: Buffer A shader, accessible as iChannel0 (optional)
- `bufferB`: Buffer B shader, accessible as iChannel1 (optional)
- `bufferC`: Buffer C shader, accessible as iChannel2 (optional)
- `bufferD`: Buffer D shader, accessible as iChannel3 (optional)

### App (`src/mcp-app.ts`)

- Receives shader code via `ontoolinput` handler
- Uses ShaderToyLite.js for WebGL rendering
- Displays compilation errors in an overlay
15 changes: 15 additions & 0 deletions examples/shadertoy-server/mcp-app.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="color-scheme" content="light dark">
<title>ShaderToy Renderer</title>
</head>
<body>
<main class="main">
<canvas id="canvas"></canvas>
</main>
<script type="module" src="/src/mcp-app.ts"></script>
</body>
</html>
43 changes: 43 additions & 0 deletions examples/shadertoy-server/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{
"name": "@modelcontextprotocol/server-shadertoy",
"version": "0.1.0",
"type": "module",
"description": "MCP App Server example for rendering ShaderToy-compatible GLSL shaders",
"repository": {
"type": "git",
"url": "https://github.com/modelcontextprotocol/ext-apps",
"directory": "examples/shadertoy-server"
},
"license": "MIT",
"main": "server.ts",
"files": [
"server.ts",
"server-utils.ts",
"dist"
],
"scripts": {
"build": "tsc --noEmit && cross-env INPUT=mcp-app.html vite build",
"watch": "cross-env INPUT=mcp-app.html vite build --watch",
"serve": "bun server.ts",
"start": "cross-env NODE_ENV=development npm run build && npm run serve",
"dev": "cross-env NODE_ENV=development concurrently 'npm run watch' 'npm run serve'",
"prepublishOnly": "npm run build"
},
"dependencies": {
"@modelcontextprotocol/ext-apps": "^0.3.1",
"@modelcontextprotocol/sdk": "^1.24.0",
"zod": "^4.1.13"
},
"devDependencies": {
"@types/cors": "^2.8.19",
"@types/express": "^5.0.0",
"@types/node": "^22.0.0",
"concurrently": "^9.2.1",
"cors": "^2.8.5",
"cross-env": "^10.1.0",
"express": "^5.1.0",
"typescript": "^5.9.3",
"vite": "^6.0.0",
"vite-plugin-singlefile": "^2.3.0"
}
}
72 changes: 72 additions & 0 deletions examples/shadertoy-server/server-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/**
* Shared utilities for running MCP servers with Streamable HTTP transport.
*/

import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import cors from "cors";
import type { Request, Response } from "express";

export interface ServerOptions {
port: number;
name?: string;
}

/**
* Starts an MCP server with Streamable HTTP transport in stateless mode.
*
* @param createServer - Factory function that creates a new McpServer instance per request.
* @param options - Server configuration options.
*/
export async function startServer(
createServer: () => McpServer,
options: ServerOptions,
): Promise<void> {
const { port, name = "MCP Server" } = options;

const app = createMcpExpressApp({ host: "0.0.0.0" });
app.use(cors());

app.all("/mcp", async (req: Request, res: Response) => {
const server = createServer();
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});

res.on("close", () => {
transport.close().catch(() => {});
server.close().catch(() => {});
});

try {
await server.connect(transport);
await transport.handleRequest(req, res, req.body);
} catch (error) {
console.error("MCP error:", error);
if (!res.headersSent) {
res.status(500).json({
jsonrpc: "2.0",
error: { code: -32603, message: "Internal server error" },
id: null,
});
}
}
});

const httpServer = app.listen(port, (err) => {
if (err) {
console.error("Failed to start server:", err);
process.exit(1);
}
console.log(`${name} listening on http://localhost:${port}/mcp`);
});

const shutdown = () => {
console.log("\nShutting down...");
httpServer.close(() => process.exit(0));
};

process.on("SIGINT", shutdown);
process.on("SIGTERM", shutdown);
}
Loading
Loading