diff --git a/.gitignore b/.gitignore index 0891fd0e..dc9ad995 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ dist/ node_modules/ yarn.lock +bun.lockb .vscode/ docs/api/ tmp/ diff --git a/examples/basic-host/src/index.tsx b/examples/basic-host/src/index.tsx index 1db4d29e..5a346fc7 100644 --- a/examples/basic-host/src/index.tsx +++ b/examples/basic-host/src/index.tsx @@ -13,6 +13,7 @@ const SERVERS = [ { name: "Customer Segmentation", port: 3105 }, { name: "Scenario Modeler", port: 3106 }, { name: "System Monitor", port: 3107 }, + { name: "Three.js", port: 3109 }, ] as const; function serverUrl(port: number): string { diff --git a/examples/threejs-server/README.md b/examples/threejs-server/README.md new file mode 100644 index 00000000..952e4e63 --- /dev/null +++ b/examples/threejs-server/README.md @@ -0,0 +1,137 @@ +# Three.js MCP Server + +Interactive 3D scene renderer using Three.js. Demonstrates streaming code preview and full MCP App integration. + +![Screenshot](https://modelcontextprotocol.github.io/ext-apps/screenshots/threejs-server/screenshot.png) + +## Tools + +| Tool | Description | +| -------------------- | ------------------------------------ | +| `show_threejs_scene` | Render 3D scene from JavaScript code | +| `learn_threejs` | Get documentation and examples | + +## Quick Start + +```bash +# Build +npm run build + +# Run (stdio mode for Claude Desktop) +bun server.ts --stdio + +# Run (HTTP mode for basic-host) +bun server.ts +``` + +## Code Structure + +``` +threejs-server/ +├── server.ts # MCP server with tools +├── mcp-app.html # Entry HTML +└── src/ + ├── mcp-app-wrapper.tsx # Generic MCP App wrapper (reusable) + ├── threejs-app.tsx # Three.js widget component + └── global.css # Styles +``` + +## Key Files + +### `src/mcp-app-wrapper.tsx` + +Generic wrapper handling MCP connection. Provides `WidgetProps` interface: + +```tsx +interface WidgetProps { + toolInputs: TToolInput | null; // Complete tool input + toolInputsPartial: TToolInput | null; // Streaming partial input + toolResult: CallToolResult | null; // Tool execution result + hostContext: McpUiHostContext | null; // Theme, viewport, locale + callServerTool: App["callServerTool"]; // Call MCP server tools + sendMessage: App["sendMessage"]; // Send chat messages + sendOpenLink: App["sendOpenLink"]; // Open URLs in browser + sendLog: App["sendLog"]; // Debug logging +} +``` + +### `src/threejs-app.tsx` + +Widget component receiving all props. Uses: + +- `toolInputs.code` - JavaScript to execute +- `toolInputsPartial.code` - Streaming preview +- `toolInputs.height` - Canvas height + +### `server.ts` + +MCP server with: + +- `show_threejs_scene` tool linked to UI resource +- `learn_threejs` documentation tool +- stdio + HTTP transport support + +## Available Three.js Globals + +```javascript +THREE; // Three.js library +canvas; // Pre-created canvas +(width, height); // Canvas dimensions +OrbitControls; // Camera controls +EffectComposer; // Post-processing +RenderPass; // Render pass +UnrealBloomPass; // Bloom effect +``` + +## Test Input + +Copy contents of `test-input.json` to test in basic-host (`http://localhost:8080`). + +## Example Code + +```javascript +const scene = new THREE.Scene(); +const camera = new THREE.PerspectiveCamera(60, width / height, 0.1, 100); +camera.position.set(2, 2, 2); + +const renderer = new THREE.WebGLRenderer({ canvas, antialias: true }); +renderer.setSize(width, height); +renderer.shadowMap.enabled = true; + +const cube = new THREE.Mesh( + new THREE.BoxGeometry(), + new THREE.MeshStandardMaterial({ color: 0x00ff88 }), +); +cube.castShadow = true; +cube.position.y = 0.5; +scene.add(cube); + +const floor = new THREE.Mesh( + new THREE.PlaneGeometry(5, 5), + new THREE.MeshStandardMaterial({ color: 0x222233 }), +); +floor.rotation.x = -Math.PI / 2; +floor.receiveShadow = true; +scene.add(floor); + +const light = new THREE.DirectionalLight(0xffffff, 2); +light.position.set(3, 5, 3); +light.castShadow = true; +scene.add(light); +scene.add(new THREE.AmbientLight(0x404040)); + +function animate() { + requestAnimationFrame(animate); + cube.rotation.y += 0.01; + renderer.render(scene, camera); +} +animate(); +``` + +## Creating a New Widget + +1. Copy this example +2. Rename `threejs-app.tsx` to your widget name +3. Define your `ToolInput` interface +4. Implement your widget using the `WidgetProps` +5. Update `server.ts` with your tools diff --git a/examples/threejs-server/mcp-app.html b/examples/threejs-server/mcp-app.html new file mode 100644 index 00000000..51f3bed2 --- /dev/null +++ b/examples/threejs-server/mcp-app.html @@ -0,0 +1,13 @@ + + + + + + Three.js Widget + + + +
+ + + diff --git a/examples/threejs-server/package.json b/examples/threejs-server/package.json new file mode 100644 index 00000000..441f941c --- /dev/null +++ b/examples/threejs-server/package.json @@ -0,0 +1,36 @@ +{ + "name": "threejs-server", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "build": "INPUT=mcp-app.html vite build", + "watch": "INPUT=mcp-app.html vite build --watch", + "serve": "bun server.ts", + "start": "NODE_ENV=development npm run build && npm run serve", + "dev": "NODE_ENV=development concurrently 'npm run watch' 'npm run serve'" + }, + "dependencies": { + "@modelcontextprotocol/ext-apps": "../..", + "@modelcontextprotocol/sdk": "^1.22.0", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "three": "^0.181.0", + "zod": "^3.25.0" + }, + "devDependencies": { + "@types/cors": "^2.8.19", + "@types/express": "^5.0.0", + "@types/node": "^22.0.0", + "@types/react": "^19.2.2", + "@types/react-dom": "^19.2.2", + "@types/three": "^0.181.0", + "@vitejs/plugin-react": "^4.3.4", + "concurrently": "^9.2.1", + "cors": "^2.8.5", + "express": "^5.1.0", + "typescript": "^5.9.3", + "vite": "^6.0.0", + "vite-plugin-singlefile": "^2.3.0" + } +} diff --git a/examples/threejs-server/server.ts b/examples/threejs-server/server.ts new file mode 100644 index 00000000..e1b07df0 --- /dev/null +++ b/examples/threejs-server/server.ts @@ -0,0 +1,240 @@ +/** + * Three.js MCP Server + * + * Provides tools for rendering interactive 3D scenes using Three.js. + */ +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import type { ReadResourceResult } from "@modelcontextprotocol/sdk/types.js"; +import cors from "cors"; +import express, { type Request, type Response } from "express"; +import fs from "node:fs/promises"; +import path from "node:path"; +import { z } from "zod"; +import { RESOURCE_URI_META_KEY } from "../../dist/src/app"; + +const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : 3109; +const DIST_DIR = path.join(import.meta.dirname, "dist"); + +const THREEJS_DOCUMENTATION = `# Three.js Widget Documentation + +## Available Globals +- \`THREE\` - Three.js library (r181) +- \`canvas\` - Pre-created canvas element +- \`width\`, \`height\` - Canvas dimensions in pixels +- \`OrbitControls\` - Interactive camera controls +- \`EffectComposer\`, \`RenderPass\`, \`UnrealBloomPass\` - Post-processing effects + +## Basic Template +\`\`\`javascript +const scene = new THREE.Scene(); +const camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000); +const renderer = new THREE.WebGLRenderer({ canvas, antialias: true }); +renderer.setSize(width, height); +renderer.setClearColor(0x1a1a2e); // Dark background + +// Add objects here... + +camera.position.z = 5; +renderer.render(scene, camera); // Static render +\`\`\` + +## Example: Rotating Cube with Lighting +\`\`\`javascript +const scene = new THREE.Scene(); +const camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000); +const renderer = new THREE.WebGLRenderer({ canvas, antialias: true }); +renderer.setSize(width, height); +renderer.setClearColor(0x1a1a2e); + +const cube = new THREE.Mesh( + new THREE.BoxGeometry(1, 1, 1), + new THREE.MeshStandardMaterial({ color: 0x00ff88 }) +); +scene.add(cube); + +// Lighting - keep intensity at 1 or below +scene.add(new THREE.DirectionalLight(0xffffff, 1)); +scene.add(new THREE.AmbientLight(0x404040)); + +camera.position.z = 3; + +function animate() { + requestAnimationFrame(animate); + cube.rotation.x += 0.01; + cube.rotation.y += 0.01; + renderer.render(scene, camera); +} +animate(); +\`\`\` + +## Example: Interactive OrbitControls +\`\`\`javascript +const scene = new THREE.Scene(); +const camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000); +const renderer = new THREE.WebGLRenderer({ canvas, antialias: true }); +renderer.setSize(width, height); +renderer.setClearColor(0x2d2d44); + +const controls = new OrbitControls(camera, renderer.domElement); +controls.enableDamping = true; + +const sphere = new THREE.Mesh( + new THREE.SphereGeometry(1, 32, 32), + new THREE.MeshStandardMaterial({ color: 0xff6b6b, roughness: 0.4 }) +); +scene.add(sphere); + +scene.add(new THREE.DirectionalLight(0xffffff, 1)); +scene.add(new THREE.AmbientLight(0x404040)); + +camera.position.z = 4; + +function animate() { + requestAnimationFrame(animate); + controls.update(); + renderer.render(scene, camera); +} +animate(); +\`\`\` + +## Tips +- Always set \`renderer.setClearColor()\` to a dark color +- Keep light intensity ≤ 1 to avoid washed-out scenes +- Use \`MeshStandardMaterial\` for realistic lighting +- For animations, use \`requestAnimationFrame\` +`; + +const server = new McpServer({ + name: "Three.js Server", + version: "1.0.0", +}); + +// Register tool and resource +{ + const resourceUri = "ui://threejs/mcp-app.html"; + + // Tool 1: show_threejs_scene + server.registerTool( + "show_threejs_scene", + { + title: "Show Three.js Scene", + description: + "Render an interactive 3D scene with custom Three.js code. Available globals: THREE, OrbitControls, EffectComposer, RenderPass, UnrealBloomPass, canvas, width, height.", + inputSchema: { + code: z.string().describe("JavaScript code to render the 3D scene"), + height: z + .number() + .int() + .positive() + .optional() + .describe("Height in pixels (default: 400)"), + }, + _meta: { [RESOURCE_URI_META_KEY]: resourceUri }, + }, + async ({ code, height }) => { + return { + content: [ + { + type: "text", + text: JSON.stringify({ code, height: height || 400 }), + }, + ], + }; + }, + ); + + // Tool 2: learn_threejs + server.registerTool( + "learn_threejs", + { + title: "Learn Three.js", + description: + "Get documentation and examples for using the Three.js widget", + inputSchema: {}, + }, + async () => { + return { + content: [{ type: "text", text: THREEJS_DOCUMENTATION }], + }; + }, + ); + + // Resource registration + server.registerResource( + resourceUri, + resourceUri, + { description: "Three.js Widget UI" }, + async (): Promise => { + const html = await fs.readFile( + path.join(DIST_DIR, "mcp-app.html"), + "utf-8", + ); + + return { + contents: [ + { + uri: resourceUri, + mimeType: "text/html;profile=mcp-app", + text: html, + }, + ], + }; + }, + ); +} + +async function main() { + if (process.argv.includes("--stdio")) { + const transport = new StdioServerTransport(); + await server.connect(transport); + console.error("Three.js Server running in stdio mode"); + } else { + const app = express(); + app.use(cors()); + app.use(express.json()); + + app.post("/mcp", async (req: Request, res: Response) => { + try { + const transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: undefined, + enableJsonResponse: true, + }); + res.on("close", () => { + transport.close(); + }); + + await server.connect(transport); + + await transport.handleRequest(req, res, req.body); + } catch (error) { + console.error("Error handling MCP request:", 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, () => { + console.log(`Three.js Server listening on http://localhost:${PORT}/mcp`); + }); + + function shutdown() { + console.log("\nShutting down..."); + httpServer.close(() => { + console.log("Server closed"); + process.exit(0); + }); + } + + process.on("SIGINT", shutdown); + process.on("SIGTERM", shutdown); + } +} + +main().catch(console.error); diff --git a/examples/threejs-server/src/global.css b/examples/threejs-server/src/global.css new file mode 100644 index 00000000..22aaaef7 --- /dev/null +++ b/examples/threejs-server/src/global.css @@ -0,0 +1,43 @@ +* { + box-sizing: border-box; +} + +html, body { + font-family: system-ui, -apple-system, sans-serif; + font-size: 1rem; + margin: 0; + padding: 0; + background: #1a1a2e; +} + +.loading { + display: flex; + align-items: center; + justify-content: center; + height: 400px; + color: #888; + font-size: 14px; +} + +.error { + padding: 20px; + color: #ff6b6b; +} + +.threejs-container { + width: 100%; + position: relative; +} + +.error-overlay { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + color: #ff6b6b; + background: rgba(26, 26, 46, 0.9); + border-radius: 8px; + font-family: system-ui; + padding: 20px; +} diff --git a/examples/threejs-server/src/mcp-app-wrapper.tsx b/examples/threejs-server/src/mcp-app-wrapper.tsx new file mode 100644 index 00000000..5e482c80 --- /dev/null +++ b/examples/threejs-server/src/mcp-app-wrapper.tsx @@ -0,0 +1,119 @@ +/** + * Three.js Widget - MCP App Wrapper + * + * Generic wrapper that handles MCP App connection and passes all relevant + * props to the actual widget component. + */ +import type { App, McpUiHostContext } from "@modelcontextprotocol/ext-apps"; +import { useApp } from "@modelcontextprotocol/ext-apps/react"; +import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; +import { StrictMode, useState, useCallback } from "react"; +import { createRoot } from "react-dom/client"; +import ThreeJSApp from "./threejs-app.tsx"; +import "./global.css"; + +const APP_INFO = { name: "Three.js Widget", version: "1.0.0" }; + +/** + * Props passed to the widget component. + * This interface can be reused for other widgets. + */ +export interface WidgetProps> { + /** Complete tool input (after streaming finishes) */ + toolInputs: TToolInput | null; + /** Partial tool input (during streaming) */ + toolInputsPartial: TToolInput | null; + /** Tool execution result from the server */ + toolResult: CallToolResult | null; + /** Host context (theme, viewport, locale, etc.) */ + hostContext: McpUiHostContext | null; + /** Call a tool on the MCP server */ + callServerTool: App["callServerTool"]; + /** Send a message to the host's chat */ + sendMessage: App["sendMessage"]; + /** Request the host to open a URL */ + sendOpenLink: App["sendOpenLink"]; + /** Send log messages to the host */ + sendLog: App["sendLog"]; +} + +function McpAppWrapper() { + const [toolInputs, setToolInputs] = useState | null>( + null, + ); + const [toolInputsPartial, setToolInputsPartial] = useState | null>(null); + const [toolResult, setToolResult] = useState(null); + const [hostContext, setHostContext] = useState(null); + + const { app, error } = useApp({ + appInfo: APP_INFO, + capabilities: {}, + onAppCreated: (app) => { + // Complete tool input (streaming finished) + app.ontoolinput = (params) => { + setToolInputs(params.arguments as Record); + setToolInputsPartial(null); + }; + // Partial tool input (streaming in progress) + app.ontoolinputpartial = (params) => { + setToolInputsPartial(params.arguments as Record); + }; + // Tool execution result + app.ontoolresult = (params) => { + setToolResult(params as CallToolResult); + }; + // Host context changes (theme, viewport, etc.) + app.onhostcontextchanged = (params) => { + setHostContext(params); + }; + }, + }); + + // Memoized callbacks that forward to app methods + const callServerTool = useCallback( + (params, options) => app!.callServerTool(params, options), + [app], + ); + const sendMessage = useCallback( + (params, options) => app!.sendMessage(params, options), + [app], + ); + const sendOpenLink = useCallback( + (params, options) => app!.sendOpenLink(params, options), + [app], + ); + const sendLog = useCallback( + (params) => app!.sendLog(params), + [app], + ); + + if (error) { + return
Error: {error.message}
; + } + + if (!app) { + return
Connecting...
; + } + + return ( + + ); +} + +createRoot(document.getElementById("root")!).render( + + + , +); diff --git a/examples/threejs-server/src/threejs-app.tsx b/examples/threejs-server/src/threejs-app.tsx new file mode 100644 index 00000000..fc45eaf5 --- /dev/null +++ b/examples/threejs-server/src/threejs-app.tsx @@ -0,0 +1,161 @@ +/** + * Three.js App Component + * + * Renders interactive 3D scenes using Three.js with streaming code preview. + * Receives all MCP App props from the wrapper. + */ +import { useState, useEffect, useRef } from "react"; +import * as THREE from "three"; +import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js"; +import { EffectComposer } from "three/examples/jsm/postprocessing/EffectComposer.js"; +import { RenderPass } from "three/examples/jsm/postprocessing/RenderPass.js"; +import { UnrealBloomPass } from "three/examples/jsm/postprocessing/UnrealBloomPass.js"; +import type { WidgetProps } from "./mcp-app-wrapper.tsx"; + +interface ThreeJSToolInput { + code?: string; + height?: number; +} + +type ThreeJSAppProps = WidgetProps; + +const SHIMMER_STYLE = ` + @keyframes shimmer { + 0% { background-position: 200% 0; } + 100% { background-position: -200% 0; } + } +`; + +function LoadingShimmer({ height, code }: { height: number; code?: string }) { + const preRef = useRef(null); + + useEffect(() => { + if (preRef.current) preRef.current.scrollTop = preRef.current.scrollHeight; + }, [code]); + + return ( +
+ +
+ 🎮 Three.js +
+ {code && ( +
+          {code}
+        
+ )} +
+ ); +} + +// Context object passed to user code +const threeContext = { + THREE, + OrbitControls, + EffectComposer, + RenderPass, + UnrealBloomPass, +}; + +async function executeThreeCode( + code: string, + canvas: HTMLCanvasElement, + width: number, + height: number, +): Promise { + const fn = new Function( + "ctx", + "canvas", + "width", + "height", + `const { THREE, OrbitControls, EffectComposer, RenderPass, UnrealBloomPass } = ctx; + return (async () => { ${code} })();`, + ); + await fn(threeContext, canvas, width, height); +} + +export default function ThreeJSApp({ + toolInputs, + toolInputsPartial, + toolResult: _toolResult, + hostContext: _hostContext, + callServerTool: _callServerTool, + sendMessage: _sendMessage, + sendOpenLink: _sendOpenLink, + sendLog: _sendLog, +}: ThreeJSAppProps) { + const [error, setError] = useState(null); + const canvasRef = useRef(null); + const containerRef = useRef(null); + + const height = toolInputs?.height ?? toolInputsPartial?.height ?? 400; + const code = toolInputs?.code; + const partialCode = toolInputsPartial?.code; + const isStreaming = !toolInputs && !!toolInputsPartial; + + useEffect(() => { + if (!code || !canvasRef.current || !containerRef.current) return; + + setError(null); + const width = containerRef.current.offsetWidth || 800; + executeThreeCode(code, canvasRef.current, width, height).catch((e) => + setError(e instanceof Error ? e.message : "Unknown error"), + ); + }, [code, height]); + + if (isStreaming || !code) { + return ; + } + + return ( +
+ + {error &&
Error: {error}
} +
+ ); +} diff --git a/examples/threejs-server/test-input.json b/examples/threejs-server/test-input.json new file mode 100644 index 00000000..54eef27a --- /dev/null +++ b/examples/threejs-server/test-input.json @@ -0,0 +1,3 @@ +{ + "code": "const scene = new THREE.Scene(); const camera = new THREE.PerspectiveCamera(60, width/height, 0.1, 100); camera.position.set(2, 2, 2); camera.lookAt(0, 0.5, 0); const renderer = new THREE.WebGLRenderer({canvas, antialias: true}); renderer.setSize(width, height); renderer.shadowMap.enabled = true; const cube = new THREE.Mesh(new THREE.BoxGeometry(), new THREE.MeshStandardMaterial({color: 0xff4444})); cube.castShadow = true; cube.position.y = 0.5; scene.add(cube); const floor = new THREE.Mesh(new THREE.PlaneGeometry(5,5), new THREE.MeshStandardMaterial({color: 0x222233})); floor.rotation.x = -Math.PI/2; floor.receiveShadow = true; scene.add(floor); const light = new THREE.DirectionalLight(0xffffff, 2); light.position.set(3, 5, 3); light.castShadow = true; scene.add(light); scene.add(new THREE.AmbientLight(0x404040)); function animate() { requestAnimationFrame(animate); cube.rotation.y += 0.01; renderer.render(scene, camera); } animate();" +} diff --git a/examples/threejs-server/tsconfig.json b/examples/threejs-server/tsconfig.json new file mode 100644 index 00000000..fc3c2101 --- /dev/null +++ b/examples/threejs-server/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ESNext", + "lib": ["ESNext", "DOM", "DOM.Iterable"], + "module": "ESNext", + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "verbatimModuleSyntax": true, + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "skipLibCheck": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src", "server.ts"] +} diff --git a/examples/threejs-server/vite.config.ts b/examples/threejs-server/vite.config.ts new file mode 100644 index 00000000..da0af84e --- /dev/null +++ b/examples/threejs-server/vite.config.ts @@ -0,0 +1,25 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; +import { viteSingleFile } from "vite-plugin-singlefile"; + +const INPUT = process.env.INPUT; +if (!INPUT) { + throw new Error("INPUT environment variable is not set"); +} + +const isDevelopment = process.env.NODE_ENV === "development"; + +export default defineConfig({ + plugins: [react(), viteSingleFile()], + build: { + sourcemap: isDevelopment ? "inline" : undefined, + cssMinify: !isDevelopment, + minify: !isDevelopment, + + rollupOptions: { + input: INPUT, + }, + outDir: "dist", + emptyOutDir: false, + }, +}); diff --git a/package-lock.json b/package-lock.json index f7e65ac0..cbf15b41 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,7 @@ "": { "name": "@modelcontextprotocol/ext-apps", "version": "0.0.1", + "license": "MIT", "workspaces": [ "examples/*" ], @@ -291,6 +292,42 @@ "undici-types": "~6.21.0" } }, + "examples/threejs-server": { + "version": "1.0.0", + "dependencies": { + "@modelcontextprotocol/ext-apps": "../..", + "@modelcontextprotocol/sdk": "^1.22.0", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "three": "^0.181.0", + "zod": "^3.25.0" + }, + "devDependencies": { + "@types/cors": "^2.8.19", + "@types/express": "^5.0.0", + "@types/node": "^22.0.0", + "@types/react": "^19.2.2", + "@types/react-dom": "^19.2.2", + "@types/three": "^0.181.0", + "@vitejs/plugin-react": "^4.3.4", + "concurrently": "^9.2.1", + "cors": "^2.8.5", + "express": "^5.1.0", + "typescript": "^5.9.3", + "vite": "^6.0.0", + "vite-plugin-singlefile": "^2.3.0" + } + }, + "examples/threejs-server/node_modules/@types/node": { + "version": "22.19.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.1.tgz", + "integrity": "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, "node_modules/@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", @@ -554,6 +591,13 @@ "node": ">=6.9.0" } }, + "node_modules/@dimforge/rapier3d-compat": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz", + "integrity": "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.12", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", @@ -1540,6 +1584,13 @@ "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", "dev": true }, + "node_modules/@tweenjs/tween.js": { + "version": "23.1.3", + "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz", + "integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -1742,12 +1793,42 @@ "@types/node": "*" } }, + "node_modules/@types/stats.js": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz", + "integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/three": { + "version": "0.181.0", + "resolved": "https://registry.npmjs.org/@types/three/-/three-0.181.0.tgz", + "integrity": "sha512-MLF1ks8yRM2k71D7RprFpDb9DOX0p22DbdPqT/uAkc6AtQXjxWCVDjCy23G9t1o8HcQPk7woD2NIyiaWcWPYmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@dimforge/rapier3d-compat": "~0.12.0", + "@tweenjs/tween.js": "~23.1.3", + "@types/stats.js": "*", + "@types/webxr": "*", + "@webgpu/types": "*", + "fflate": "~0.8.2", + "meshoptimizer": "~0.22.0" + } + }, "node_modules/@types/unist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", "dev": true }, + "node_modules/@types/webxr": { + "version": "0.5.24", + "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz", + "integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==", + "dev": true, + "license": "MIT" + }, "node_modules/@vitejs/plugin-react": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", @@ -1876,6 +1957,13 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/@webgpu/types": { + "version": "0.1.67", + "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.67.tgz", + "integrity": "sha512-uk53+2ECGUkWoDFez/hymwpRfdgdIn6y1ref70fEecGMe5607f4sozNFgBk0oxlr7j2CRGWBEc3IBYMmFdGGTQ==", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/accepts": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", @@ -2716,6 +2804,13 @@ } ] }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "dev": true, + "license": "MIT" + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -3154,6 +3249,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/meshoptimizer": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-0.22.0.tgz", + "integrity": "sha512-IebiK79sqIy+E4EgOr+CAw+Ke8hAspXKzBd0JdgEmPHiAwmvEj2S4h1rfvo+o/BnfEYd/jAOg5IeeIjzlzSnDg==", + "dev": true, + "license": "MIT" + }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -3950,6 +4052,16 @@ "url": "https://www.buymeacoffee.com/systeminfo" } }, + "node_modules/three": { + "version": "0.181.2", + "resolved": "https://registry.npmjs.org/three/-/three-0.181.2.tgz", + "integrity": "sha512-k/CjiZ80bYss6Qs7/ex1TBlPD11whT9oKfT8oTGiHa34W4JRd1NiH/Tr1DbHWQ2/vMUypxksLnF2CfmlmM5XFQ==", + "license": "MIT" + }, + "node_modules/threejs-server": { + "resolved": "examples/threejs-server", + "link": true + }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", diff --git a/package.json b/package.json index 27737b5b..832040cb 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "build:all": "npm run build && npm run examples:build", "test": "bun test", "examples:build": "find examples -maxdepth 1 -mindepth 1 -type d -exec printf '%s\\0' 'npm run --workspace={} build' ';' | xargs -0 concurrently --kill-others-on-fail", - "examples:start": "NODE_ENV=development npm run build && concurrently 'npm run examples:start:basic-host' 'npm run examples:start:basic-server-react' 'npm run examples:start:basic-server-vanillajs' 'npm run examples:start:budget-allocator-server' 'npm run examples:start:cohort-heatmap-server' 'npm run examples:start:customer-segmentation-server' 'npm run examples:start:scenario-modeler-server' 'npm run examples:start:system-monitor-server'", + "examples:start": "NODE_ENV=development npm run build && concurrently 'npm run examples:start:basic-host' 'npm run examples:start:basic-server-react' 'npm run examples:start:basic-server-vanillajs' 'npm run examples:start:budget-allocator-server' 'npm run examples:start:cohort-heatmap-server' 'npm run examples:start:customer-segmentation-server' 'npm run examples:start:scenario-modeler-server' 'npm run examples:start:system-monitor-server' 'npm run examples:start:threejs-server'", "examples:start:basic-host": "npm run --workspace=examples/basic-host start", "examples:start:basic-server-react": "PORT=3101 npm run --workspace=examples/basic-server-react start", "examples:start:basic-server-vanillajs": "PORT=3102 npm run --workspace=examples/basic-server-vanillajs start", @@ -40,6 +40,7 @@ "examples:start:customer-segmentation-server": "PORT=3105 npm run --workspace=examples/customer-segmentation-server start", "examples:start:scenario-modeler-server": "PORT=3106 npm run --workspace=examples/scenario-modeler-server start", "examples:start:system-monitor-server": "PORT=3107 npm run --workspace=examples/system-monitor-server start", + "examples:start:threejs-server": "PORT=3109 npm run --workspace=examples/threejs-server start", "watch": "nodemon --watch src --ext ts,tsx --exec 'bun build.bun.ts'", "examples:dev": "NODE_ENV=development concurrently 'npm run watch' 'npm run examples:dev:basic-host' 'npm run examples:dev:basic-server-react'", "examples:dev:basic-host": "npm run --workspace=examples/basic-host dev",