Merge pull request #94 from modelcontextprotocol/add-threejs-server
Add Three.js example server
This commit is contained in:
commit
2b53efda5a
14 changed files with 913 additions and 1 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -2,6 +2,7 @@
|
||||||
dist/
|
dist/
|
||||||
node_modules/
|
node_modules/
|
||||||
yarn.lock
|
yarn.lock
|
||||||
|
bun.lockb
|
||||||
.vscode/
|
.vscode/
|
||||||
docs/api/
|
docs/api/
|
||||||
tmp/
|
tmp/
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ const SERVERS = [
|
||||||
{ name: "Customer Segmentation", port: 3105 },
|
{ name: "Customer Segmentation", port: 3105 },
|
||||||
{ name: "Scenario Modeler", port: 3106 },
|
{ name: "Scenario Modeler", port: 3106 },
|
||||||
{ name: "System Monitor", port: 3107 },
|
{ name: "System Monitor", port: 3107 },
|
||||||
|
{ name: "Three.js", port: 3109 },
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
function serverUrl(port: number): string {
|
function serverUrl(port: number): string {
|
||||||
|
|
|
||||||
137
examples/threejs-server/README.md
Normal file
137
examples/threejs-server/README.md
Normal file
|
|
@ -0,0 +1,137 @@
|
||||||
|
# Three.js MCP Server
|
||||||
|
|
||||||
|
Interactive 3D scene renderer using Three.js. Demonstrates streaming code preview and full MCP App integration.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 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<TToolInput> {
|
||||||
|
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
|
||||||
13
examples/threejs-server/mcp-app.html
Normal file
13
examples/threejs-server/mcp-app.html
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Three.js Widget</title>
|
||||||
|
<link rel="stylesheet" href="/src/global.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/mcp-app-wrapper.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
36
examples/threejs-server/package.json
Normal file
36
examples/threejs-server/package.json
Normal file
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
240
examples/threejs-server/server.ts
Normal file
240
examples/threejs-server/server.ts
Normal file
|
|
@ -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<ReadResourceResult> => {
|
||||||
|
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);
|
||||||
43
examples/threejs-server/src/global.css
Normal file
43
examples/threejs-server/src/global.css
Normal file
|
|
@ -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;
|
||||||
|
}
|
||||||
119
examples/threejs-server/src/mcp-app-wrapper.tsx
Normal file
119
examples/threejs-server/src/mcp-app-wrapper.tsx
Normal file
|
|
@ -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<TToolInput = Record<string, unknown>> {
|
||||||
|
/** 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<Record<string, unknown> | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
const [toolInputsPartial, setToolInputsPartial] = useState<Record<
|
||||||
|
string,
|
||||||
|
unknown
|
||||||
|
> | null>(null);
|
||||||
|
const [toolResult, setToolResult] = useState<CallToolResult | null>(null);
|
||||||
|
const [hostContext, setHostContext] = useState<McpUiHostContext | null>(null);
|
||||||
|
|
||||||
|
const { app, error } = useApp({
|
||||||
|
appInfo: APP_INFO,
|
||||||
|
capabilities: {},
|
||||||
|
onAppCreated: (app) => {
|
||||||
|
// Complete tool input (streaming finished)
|
||||||
|
app.ontoolinput = (params) => {
|
||||||
|
setToolInputs(params.arguments as Record<string, unknown>);
|
||||||
|
setToolInputsPartial(null);
|
||||||
|
};
|
||||||
|
// Partial tool input (streaming in progress)
|
||||||
|
app.ontoolinputpartial = (params) => {
|
||||||
|
setToolInputsPartial(params.arguments as Record<string, unknown>);
|
||||||
|
};
|
||||||
|
// 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<App["callServerTool"]>(
|
||||||
|
(params, options) => app!.callServerTool(params, options),
|
||||||
|
[app],
|
||||||
|
);
|
||||||
|
const sendMessage = useCallback<App["sendMessage"]>(
|
||||||
|
(params, options) => app!.sendMessage(params, options),
|
||||||
|
[app],
|
||||||
|
);
|
||||||
|
const sendOpenLink = useCallback<App["sendOpenLink"]>(
|
||||||
|
(params, options) => app!.sendOpenLink(params, options),
|
||||||
|
[app],
|
||||||
|
);
|
||||||
|
const sendLog = useCallback<App["sendLog"]>(
|
||||||
|
(params) => app!.sendLog(params),
|
||||||
|
[app],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <div className="error">Error: {error.message}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!app) {
|
||||||
|
return <div className="loading">Connecting...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ThreeJSApp
|
||||||
|
toolInputs={toolInputs}
|
||||||
|
toolInputsPartial={toolInputsPartial}
|
||||||
|
toolResult={toolResult}
|
||||||
|
hostContext={hostContext}
|
||||||
|
callServerTool={callServerTool}
|
||||||
|
sendMessage={sendMessage}
|
||||||
|
sendOpenLink={sendOpenLink}
|
||||||
|
sendLog={sendLog}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
createRoot(document.getElementById("root")!).render(
|
||||||
|
<StrictMode>
|
||||||
|
<McpAppWrapper />
|
||||||
|
</StrictMode>,
|
||||||
|
);
|
||||||
161
examples/threejs-server/src/threejs-app.tsx
Normal file
161
examples/threejs-server/src/threejs-app.tsx
Normal file
|
|
@ -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<ThreeJSToolInput>;
|
||||||
|
|
||||||
|
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<HTMLPreElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (preRef.current) preRef.current.scrollTop = preRef.current.scrollHeight;
|
||||||
|
}, [code]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
height,
|
||||||
|
borderRadius: 8,
|
||||||
|
padding: 16,
|
||||||
|
boxSizing: "border-box",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
overflow: "hidden",
|
||||||
|
background:
|
||||||
|
"linear-gradient(90deg, #1a1a2e 25%, #2d2d44 50%, #1a1a2e 75%)",
|
||||||
|
backgroundSize: "200% 100%",
|
||||||
|
animation: "shimmer 1.5s ease-in-out infinite",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<style>{SHIMMER_STYLE}</style>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
color: "#888",
|
||||||
|
fontFamily: "system-ui",
|
||||||
|
fontSize: 12,
|
||||||
|
marginBottom: 8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
🎮 Three.js
|
||||||
|
</div>
|
||||||
|
{code && (
|
||||||
|
<pre
|
||||||
|
ref={preRef}
|
||||||
|
style={{
|
||||||
|
margin: 0,
|
||||||
|
padding: 0,
|
||||||
|
flex: 1,
|
||||||
|
overflow: "auto",
|
||||||
|
color: "#aaa",
|
||||||
|
fontFamily: "monospace",
|
||||||
|
fontSize: 11,
|
||||||
|
lineHeight: 1.4,
|
||||||
|
whiteSpace: "pre-wrap",
|
||||||
|
wordBreak: "break-word",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{code}
|
||||||
|
</pre>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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<void> {
|
||||||
|
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<string | null>(null);
|
||||||
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
|
const containerRef = useRef<HTMLDivElement>(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 <LoadingShimmer height={height} code={partialCode} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={containerRef} className="threejs-container">
|
||||||
|
<canvas
|
||||||
|
ref={canvasRef}
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
height,
|
||||||
|
borderRadius: 8,
|
||||||
|
display: "block",
|
||||||
|
background: "#1a1a2e",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{error && <div className="error-overlay">Error: {error}</div>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
3
examples/threejs-server/test-input.json
Normal file
3
examples/threejs-server/test-input.json
Normal file
|
|
@ -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();"
|
||||||
|
}
|
||||||
20
examples/threejs-server/tsconfig.json
Normal file
20
examples/threejs-server/tsconfig.json
Normal file
|
|
@ -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"]
|
||||||
|
}
|
||||||
25
examples/threejs-server/vite.config.ts
Normal file
25
examples/threejs-server/vite.config.ts
Normal file
|
|
@ -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,
|
||||||
|
},
|
||||||
|
});
|
||||||
112
package-lock.json
generated
112
package-lock.json
generated
|
|
@ -7,6 +7,7 @@
|
||||||
"": {
|
"": {
|
||||||
"name": "@modelcontextprotocol/ext-apps",
|
"name": "@modelcontextprotocol/ext-apps",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
|
"license": "MIT",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"examples/*"
|
"examples/*"
|
||||||
],
|
],
|
||||||
|
|
@ -291,6 +292,42 @@
|
||||||
"undici-types": "~6.21.0"
|
"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": {
|
"node_modules/@babel/code-frame": {
|
||||||
"version": "7.27.1",
|
"version": "7.27.1",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
|
||||||
|
|
@ -554,6 +591,13 @@
|
||||||
"node": ">=6.9.0"
|
"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": {
|
"node_modules/@esbuild/aix-ppc64": {
|
||||||
"version": "0.25.12",
|
"version": "0.25.12",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
|
||||||
|
|
@ -1551,6 +1595,13 @@
|
||||||
"integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==",
|
"integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/@types/babel__core": {
|
||||||
"version": "7.20.5",
|
"version": "7.20.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
||||||
|
|
@ -1753,12 +1804,42 @@
|
||||||
"@types/node": "*"
|
"@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": {
|
"node_modules/@types/unist": {
|
||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
|
||||||
"integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
|
"integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/@vitejs/plugin-react": {
|
||||||
"version": "4.7.0",
|
"version": "4.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
|
||||||
|
|
@ -1887,6 +1968,13 @@
|
||||||
"url": "https://opencollective.com/vitest"
|
"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": {
|
"node_modules/accepts": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
|
||||||
|
|
@ -2728,6 +2816,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": {
|
"node_modules/fill-range": {
|
||||||
"version": "7.1.1",
|
"version": "7.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||||
|
|
@ -3166,6 +3261,13 @@
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"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": {
|
"node_modules/micromatch": {
|
||||||
"version": "4.0.8",
|
"version": "4.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
||||||
|
|
@ -3962,6 +4064,16 @@
|
||||||
"url": "https://www.buymeacoffee.com/systeminfo"
|
"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": {
|
"node_modules/tinybench": {
|
||||||
"version": "2.9.0",
|
"version": "2.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@
|
||||||
"build:all": "npm run build && npm run examples:build",
|
"build:all": "npm run build && npm run examples:build",
|
||||||
"test": "bun test",
|
"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: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-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-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",
|
"examples:start:basic-server-vanillajs": "PORT=3102 npm run --workspace=examples/basic-server-vanillajs start",
|
||||||
|
|
@ -41,6 +41,7 @@
|
||||||
"examples:start:customer-segmentation-server": "PORT=3105 npm run --workspace=examples/customer-segmentation-server start",
|
"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: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: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'",
|
"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": "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",
|
"examples:dev:basic-host": "npm run --workspace=examples/basic-host dev",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue