Skip to content

Commit 756a0a8

Browse files
committed
feat(examples): add OpenSCAD web renderer demo
Add a new MCP App Server example that renders OpenSCAD 3D models: - Uses openscad-wasm to compile OpenSCAD code to STL in the browser - Renders output with Three.js and OrbitControls for interaction - Supports streaming preview while code is being generated - Includes two tools: show_openscad_model and learn_openscad - Uses Manifold backend for fast compilation
1 parent 43c30a7 commit 756a0a8

File tree

12 files changed

+1333
-0
lines changed

12 files changed

+1333
-0
lines changed

examples/openscad-server/README.md

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
# OpenSCAD MCP App Server
2+
3+
A 3D modeling MCP App Server that renders OpenSCAD code using WebAssembly and Three.js.
4+
5+
## Features
6+
7+
- **WebAssembly Compilation**: Uses [openscad-wasm](https://github.com/openscad/openscad-wasm) to compile OpenSCAD code in the browser
8+
- **3D Rendering**: Renders STL output with Three.js and OrbitControls for interaction
9+
- **Streaming Preview**: Shows code as it streams from the LLM
10+
- **Fast Rendering**: Uses the Manifold backend for optimized compilation
11+
12+
## Tools
13+
14+
### `show_openscad_model`
15+
16+
Renders a 3D model from OpenSCAD code.
17+
18+
**Parameters:**
19+
- `code` (string): OpenSCAD code to compile and render
20+
- `height` (number, optional): Height of the viewer in pixels (default: 500)
21+
22+
### `learn_openscad`
23+
24+
Returns documentation and examples for using OpenSCAD.
25+
26+
## Running the Server
27+
28+
### HTTP Mode (default)
29+
```bash
30+
npm run start
31+
# Server will be available at http://localhost:3120/mcp
32+
```
33+
34+
### Stdio Mode
35+
```bash
36+
npm run start:stdio
37+
```
38+
39+
### Development Mode
40+
```bash
41+
npm run dev
42+
```
43+
44+
## MCP Client Configuration
45+
46+
### Claude Desktop
47+
48+
Add to your Claude Desktop configuration file:
49+
50+
**macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
51+
**Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
52+
53+
```json
54+
{
55+
"mcpServers": {
56+
"openscad": {
57+
"url": "http://localhost:3120/mcp"
58+
}
59+
}
60+
}
61+
```
62+
63+
## Example Usage
64+
65+
Ask Claude to create 3D models:
66+
67+
- "Create a parametric gear with 20 teeth"
68+
- "Design a simple box with a hinged lid"
69+
- "Make a spiral vase using rotate_extrude"
70+
- "Create a chess pawn piece"
71+
72+
## OpenSCAD Quick Reference
73+
74+
### Basic Shapes
75+
```openscad
76+
cube([10, 20, 30]); // Box
77+
sphere(r = 10); // Sphere
78+
cylinder(h = 20, r = 5); // Cylinder
79+
```
80+
81+
### Transformations
82+
```openscad
83+
translate([x, y, z]) object();
84+
rotate([rx, ry, rz]) object();
85+
scale([sx, sy, sz]) object();
86+
```
87+
88+
### Boolean Operations
89+
```openscad
90+
union() { a(); b(); } // Combine
91+
difference() { a(); b(); } // Subtract b from a
92+
intersection() { a(); b(); } // Overlap only
93+
```
94+
95+
### Modules
96+
```openscad
97+
module my_shape(size = 10) {
98+
cube([size, size, size]);
99+
}
100+
my_shape(size = 20);
101+
```
102+
103+
## Credits
104+
105+
- [OpenSCAD](https://openscad.org/) - The original 3D CAD modeler
106+
- [openscad-wasm](https://github.com/openscad/openscad-wasm) - WebAssembly port by @DSchroer
107+
- [Three.js](https://threejs.org/) - 3D rendering library

examples/openscad-server/main.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/**
2+
* Entry point for running the MCP server.
3+
* Run with: npx mcp-openscad-server
4+
* Or: node dist/index.js [--stdio]
5+
*/
6+
7+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
8+
import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
9+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
10+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
11+
import cors from "cors";
12+
import type { Request, Response } from "express";
13+
import { createServer } from "./server.js";
14+
15+
export interface ServerOptions {
16+
port: number;
17+
name?: string;
18+
}
19+
20+
/**
21+
* Starts an MCP server with Streamable HTTP transport in stateless mode.
22+
*
23+
* @param createServer - Factory function that creates a new McpServer instance per request.
24+
* @param options - Server configuration options.
25+
*/
26+
export async function startServer(
27+
createServer: () => McpServer,
28+
options: ServerOptions,
29+
): Promise<void> {
30+
const { port, name = "MCP Server" } = options;
31+
32+
const app = createMcpExpressApp({ host: "0.0.0.0" });
33+
app.use(cors());
34+
35+
app.all("/mcp", async (req: Request, res: Response) => {
36+
const server = createServer();
37+
const transport = new StreamableHTTPServerTransport({
38+
sessionIdGenerator: undefined,
39+
});
40+
41+
res.on("close", () => {
42+
transport.close().catch(() => {});
43+
server.close().catch(() => {});
44+
});
45+
46+
try {
47+
await server.connect(transport);
48+
await transport.handleRequest(req, res, req.body);
49+
} catch (error) {
50+
console.error("MCP error:", error);
51+
if (!res.headersSent) {
52+
res.status(500).json({
53+
jsonrpc: "2.0",
54+
error: { code: -32603, message: "Internal server error" },
55+
id: null,
56+
});
57+
}
58+
}
59+
});
60+
61+
const httpServer = app.listen(port, (err) => {
62+
if (err) {
63+
console.error("Failed to start server:", err);
64+
process.exit(1);
65+
}
66+
console.log(`${name} listening on http://localhost:${port}/mcp`);
67+
});
68+
69+
const shutdown = () => {
70+
console.log("\nShutting down...");
71+
httpServer.close(() => process.exit(0));
72+
};
73+
74+
process.on("SIGINT", shutdown);
75+
process.on("SIGTERM", shutdown);
76+
}
77+
78+
async function main() {
79+
if (process.argv.includes("--stdio")) {
80+
await createServer().connect(new StdioServerTransport());
81+
} else {
82+
const port = parseInt(process.env.PORT ?? "3120", 10);
83+
await startServer(createServer, { port, name: "OpenSCAD Server" });
84+
}
85+
}
86+
87+
main().catch((e) => {
88+
console.error(e);
89+
process.exit(1);
90+
});
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
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>OpenSCAD Widget</title>
8+
<link rel="stylesheet" href="/src/global.css">
9+
</head>
10+
<body>
11+
<div id="root"></div>
12+
<script type="module" src="/src/mcp-app-wrapper.tsx"></script>
13+
</body>
14+
</html>
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
{
2+
"name": "@modelcontextprotocol/server-openscad",
3+
"version": "0.4.1",
4+
"type": "module",
5+
"description": "OpenSCAD 3D modeling MCP App Server",
6+
"repository": {
7+
"type": "git",
8+
"url": "https://github.com/modelcontextprotocol/ext-apps",
9+
"directory": "examples/openscad-server"
10+
},
11+
"license": "MIT",
12+
"main": "dist/server.js",
13+
"files": [
14+
"dist"
15+
],
16+
"scripts": {
17+
"build": "tsc --noEmit && cross-env INPUT=mcp-app.html vite build && tsc -p tsconfig.server.json && bun build server.ts --outdir dist --target node && bun build main.ts --outfile dist/index.js --target node --external \"./server.js\" --banner \"#!/usr/bin/env node\"",
18+
"watch": "cross-env INPUT=mcp-app.html vite build --watch",
19+
"serve:http": "bun --watch main.ts",
20+
"serve:stdio": "bun server.ts --stdio",
21+
"start": "npm run start:http",
22+
"start:http": "cross-env NODE_ENV=development npm run build && npm run serve:http",
23+
"start:stdio": "cross-env NODE_ENV=development npm run build && npm run serve:stdio",
24+
"dev": "cross-env NODE_ENV=development concurrently 'npm run watch' 'npm run serve:http'",
25+
"prepublishOnly": "npm run build",
26+
"serve": "bun --watch main.ts"
27+
},
28+
"dependencies": {
29+
"@modelcontextprotocol/ext-apps": "^0.4.1",
30+
"@modelcontextprotocol/sdk": "^1.24.0",
31+
"cors": "^2.8.5",
32+
"express": "^5.1.0",
33+
"react": "^19.2.0",
34+
"react-dom": "^19.2.0",
35+
"three": "^0.181.0",
36+
"zod": "^4.1.13"
37+
},
38+
"devDependencies": {
39+
"@types/cors": "^2.8.19",
40+
"@types/express": "^5.0.0",
41+
"@types/node": "^22.0.0",
42+
"@types/react": "^19.2.2",
43+
"@types/react-dom": "^19.2.2",
44+
"@types/three": "^0.181.0",
45+
"@vitejs/plugin-react": "^4.3.4",
46+
"concurrently": "^9.2.1",
47+
"cross-env": "^10.1.0",
48+
"typescript": "^5.9.3",
49+
"vite": "^6.0.0",
50+
"vite-plugin-singlefile": "^2.3.0"
51+
},
52+
"types": "dist/server.d.ts",
53+
"exports": {
54+
".": {
55+
"types": "./dist/server.d.ts",
56+
"default": "./dist/server.js"
57+
}
58+
},
59+
"bin": {
60+
"mcp-openscad-server": "dist/index.js"
61+
}
62+
}

0 commit comments

Comments
 (0)