openscad-mcp-server 1.0.4 → 1.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,7 @@
1
1
  # OpenSCAD MCP Server
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/openscad-mcp-server.svg)](https://www.npmjs.com/package/openscad-mcp-server)
4
+ [![npm downloads](https://img.shields.io/npm/dt/openscad-mcp-server.svg)](https://www.npmjs.com/package/openscad-mcp-server)
4
5
  [![CI](https://github.com/fboldo/openscad-mcp-server/actions/workflows/ci.yml/badge.svg)](https://github.com/fboldo/openscad-mcp-server/actions/workflows/ci.yml)
5
6
 
6
7
  An MCP (Model Context Protocol) server that renders **PNG previews** and **STL geometry** from OpenSCAD (SCAD) source code. It is designed to support **iterative, agent-driven CAD workflows**, where models can be previewed visually and exported for downstream use (e.g. fabrication, simulation, or inspection).
@@ -33,6 +34,16 @@ This MCP server is currently in beta. Performance, APIs, and capabilities may ch
33
34
  - Input: `scadCode` (string), optional `filename` (string)
34
35
  - Output: MCP embedded resource (STL)
35
36
 
37
+ ## Skill
38
+
39
+ This repository also includes an [OpenSCAD iterative modeling skill](skills/openscad-iterative-modeling/SKILL.md) that demonstrates how to use this MCP server to support an iterative SCAD → PNG → critique → refine loop.
40
+
41
+ ## Limitations
42
+
43
+ - **Performance**: Rendering complex SCAD models can be slow, especially in a WASM environment.
44
+ - **Feature support**: Not all OpenSCAD features may be fully supported or may have limitations in the WASM version.
45
+ - **Fonts**: Text rendering is not currently supported. Support is planned for a future release.
46
+
36
47
  ## Installation
37
48
 
38
49
  The published package is intended to run over stdio. Configure it in your MCP client using `npx`:
@@ -48,11 +59,29 @@ The published package is intended to run over stdio. Configure it in your MCP cl
48
59
  }
49
60
  ```
50
61
 
62
+ To run it over HTTP instead (e.g. in Docker), pass `--http`:
63
+
64
+ ```bash
65
+ MCP_PORT=3000 npx -y openscad-mcp-server --http
66
+ ```
67
+
68
+ The server exposes `GET /health` and a stateless streamable HTTP MCP endpoint at `POST /mcp`.
69
+
70
+ ### Using the Skill
71
+
72
+ [Agents skills](https://github.com/agentskills/agentskills) are a simple, open format for giving agents new capabilities and expertise.
73
+
74
+ The most straightforward to use the OpenSCAD iterative modeling skill is to install it using the [skills CLI](https://github.com/vercel-labs/skills):
75
+
76
+ ```bash
77
+ npx skills add fboldo/openscad-mcp-server --skill openscad-iterative-modeling
78
+ ```
79
+
51
80
  ## Local development
52
81
 
53
82
  - Install deps: `bun install`
54
83
  - Stdio (matches how clients run it): `bun index.ts --stdio`
55
- - HTTP (useful for manual testing): `bun index.ts`
84
+ - HTTP (useful for manual testing): `bun index.ts` (or `--http`)
56
85
  - Port: `MCP_PORT` (default `3000`)
57
86
  - Endpoints: `GET /health`, MCP at `POST /mcp`
58
87
  - MCP Inspector: `bun run dev`
@@ -62,6 +91,9 @@ The published package is intended to run over stdio. Configure it in your MCP cl
62
91
  - [jhacksman/OpenSCAD-MCP-Server](https://github.com/jhacksman/OpenSCAD-MCP-Server)
63
92
  This project provides a different approach relying on generating images from user prompts, followed by 3D reconstruction and even 3D printer discovery. It's a very interesting project, and I recommend checking it out if you are interested in OpenSCAD and MCP servers.
64
93
 
94
+ - [petrijr/openscad-mcp](https://github.com/petrijr/openscad-mcp)
95
+ Similar to this project, but it uses a Python-based server and relies on the OpenSCAD CLI for rendering.
96
+
65
97
  ## Relevant Links
66
98
 
67
99
  - [OpenSCAD](https://openscad.org/)
package/dist/index.mjs CHANGED
@@ -58,24 +58,48 @@ const asTool = (fn) => {
58
58
 
59
59
  //#endregion
60
60
  //#region src/infra/openscad.ts
61
- const getOpenSCADInstance = async () => {
62
- const writeStderr = (line) => {
63
- try {
64
- process.stderr.write(line.endsWith("\n") ? line : `${line}\n`);
65
- } catch {
66
- console.error(line);
61
+ /**
62
+ * Executes a callback function with an OpenSCAD instance, capturing any errors and outputs produced during the execution.
63
+ *
64
+ * @param callback - An async function that receives an OpenSCAD instance to perform operations
65
+ * @returns A tuple containing the callback result (or error), an array of errors captured from OpenSCAD's stderr, and a set of outputs captured from OpenSCAD's stdout
66
+ */
67
+ const executeOpenSCAD = async (callback) => {
68
+ const outputs = [];
69
+ const response = await callback(await createOpenSCAD({
70
+ noInitialRun: true,
71
+ printErr: (text) => {
72
+ outputs.push(text);
73
+ },
74
+ print: (text) => {
75
+ outputs.push(text);
67
76
  }
68
- };
69
- return await createOpenSCAD({
70
- print: (text) => writeStderr(`[OpenSCAD]: ${text}`),
71
- printErr: (text) => writeStderr(`[OpenSCAD Error]: ${text}`)
77
+ })).catch((e) => {
78
+ return e;
72
79
  });
80
+ if (response.constructor.name.includes("Error")) {
81
+ const errors = outputs.filter((o) => o.toLowerCase().includes("error:"));
82
+ const errorMessage = errors.length > 0 ? [response.message, ...errors].filter(Boolean).join("\n") : response.message;
83
+ throw new Error(errorMessage);
84
+ }
85
+ return response;
73
86
  };
74
87
 
75
88
  //#endregion
76
89
  //#region src/app/tools/export-scad-stl/tool.ts
77
90
  const exportScadStlTool = async ({ filename = "model.stl", scadCode }) => {
78
- const stl = await (await getOpenSCADInstance()).renderToStl(scadCode);
91
+ const result = await executeOpenSCAD(async (instance) => {
92
+ const openscad = instance.getInstance();
93
+ openscad.FS.writeFile("input.scad", scadCode);
94
+ openscad.callMain([
95
+ "input.scad",
96
+ "-o",
97
+ "output.stl",
98
+ "--backend=manifold"
99
+ ]);
100
+ return openscad.FS.readFile("output.stl");
101
+ });
102
+ if (!result) throw new Error("Failed to generate STL from OpenSCAD");
79
103
  const assignedFilename = filename.endsWith(".stl") ? filename : `${filename}.stl`;
80
104
  const defaultFilename = `model-${(/* @__PURE__ */ new Date()).getTime()}.stl`;
81
105
  return {
@@ -83,7 +107,7 @@ const exportScadStlTool = async ({ filename = "model.stl", scadCode }) => {
83
107
  resource: {
84
108
  uri: `file://${assignedFilename ?? defaultFilename}`,
85
109
  mimeType: "model/stl",
86
- blob: Buffer.from(stl, "utf8").toString("base64")
110
+ blob: Buffer.from(result, "utf8").toString("base64")
87
111
  }
88
112
  };
89
113
  };
@@ -172,9 +196,21 @@ const resolveCameraPosition = (cameraPosition, cameraPreset) => {
172
196
  return CAMERA_PRESETS[cameraPreset];
173
197
  };
174
198
  const renderScadPngTool = async ({ scadCode, width, height, cameraPreset, cameraPosition }) => {
199
+ const result = await executeOpenSCAD(async (instance) => {
200
+ const openscad = instance.getInstance();
201
+ openscad.FS.writeFile("input.scad", scadCode);
202
+ openscad.callMain([
203
+ "input.scad",
204
+ "-o",
205
+ "output.stl",
206
+ "--backend=manifold"
207
+ ]);
208
+ return openscad.FS.readFile("output.stl");
209
+ });
210
+ if (!result) throw new Error("Failed to generate STL from OpenSCAD");
175
211
  return {
176
212
  type: "image",
177
- data: await createPngBase64FromStl(await (await getOpenSCADInstance()).renderToStl(scadCode), width, height, resolveCameraPosition(cameraPosition, cameraPreset)),
213
+ data: await createPngBase64FromStl(result, width, height, resolveCameraPosition(cameraPosition, cameraPreset)),
178
214
  mimeType: "image/png",
179
215
  _meta: {
180
216
  width,
@@ -208,20 +244,6 @@ const RenderScadPngToolInputSchema = z.object({
208
244
  cameraPreset: RenderScadPngCameraPreset.optional().describe("A named camera preset used when `cameraPosition` is not provided"),
209
245
  cameraPosition: RenderScadPngCamera.optional().describe("Camera position as { x,y,z }. Example: { x: 0, y: -25, z: 20 }")
210
246
  });
211
- const RenderScadPngToolOutputSchema = z.object({
212
- image: ImageContentSchema,
213
- metadata: z.object({
214
- width: z.number(),
215
- height: z.number(),
216
- cameraPreset: RenderScadPngCameraPreset.optional(),
217
- cameraPosition: RenderScadPngCamera.optional(),
218
- timingsMs: z.object({
219
- openscadToStl: z.number(),
220
- stlToPng: z.number(),
221
- total: z.number()
222
- })
223
- })
224
- });
225
247
 
226
248
  //#endregion
227
249
  //#region src/app/tools/render-scad-png/register.ts
@@ -251,8 +273,13 @@ function createServer() {
251
273
 
252
274
  //#endregion
253
275
  //#region src/entrypoints/http.ts
254
- const createHttpServer = async (createServer) => {
255
- const transport = new WebStandardStreamableHTTPServerTransport();
276
+ /**
277
+ * Creates the Hono app serving the MCP endpoint in stateless mode.
278
+ *
279
+ * Stateless transports cannot be reused across requests, so a fresh server and
280
+ * transport pair is created for every `/mcp` request and closed once it has responded.
281
+ */
282
+ const createHttpApp = (createServer) => {
256
283
  const app = new Hono();
257
284
  app.use("*", cors({
258
285
  origin: "*",
@@ -271,9 +298,24 @@ const createHttpServer = async (createServer) => {
271
298
  exposeHeaders: ["mcp-session-id", "mcp-protocol-version"]
272
299
  }));
273
300
  app.get("/health", (c) => c.json({ status: "ok" }));
274
- app.all("/mcp", (c) => transport.handleRequest(c.req.raw));
301
+ app.all("/mcp", async (c) => {
302
+ const transport = new WebStandardStreamableHTTPServerTransport({
303
+ sessionIdGenerator: void 0,
304
+ enableJsonResponse: true
305
+ });
306
+ const server = createServer();
307
+ try {
308
+ await server.connect(transport);
309
+ return await transport.handleRequest(c.req.raw);
310
+ } finally {
311
+ await server.close();
312
+ }
313
+ });
314
+ return app;
315
+ };
316
+ const createHttpServer = async (createServer) => {
317
+ const app = createHttpApp(createServer);
275
318
  const PORT = process.env.MCP_PORT ? Number.parseInt(process.env.MCP_PORT, 10) : 3e3;
276
- await createServer().connect(transport);
277
319
  console.log(`Starting Hono MCP server on port ${PORT}`);
278
320
  console.log(`Health check: http://localhost:${PORT}/health`);
279
321
  console.log(`MCP endpoint: http://localhost:${PORT}/mcp`);
@@ -308,7 +350,16 @@ const bootstrap = (method, createServer) => {
308
350
 
309
351
  //#endregion
310
352
  //#region index.ts
311
- await bootstrap((process.argv.includes("--stdio"), "stdio"), createServer);
353
+ /**
354
+ * Explicit `--http` / `--stdio` flags win. Without a flag, the published build
355
+ * (NODE_ENV=production) defaults to stdio and local development defaults to HTTP.
356
+ */
357
+ const resolveBootstrapMethod = (argv) => {
358
+ if (argv.includes("--http")) return "http";
359
+ if (argv.includes("--stdio")) return "stdio";
360
+ return "stdio";
361
+ };
362
+ await bootstrap(resolveBootstrapMethod(process.argv), createServer);
312
363
 
313
364
  //#endregion
314
365
  export { };
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "module": "./dist/index.mjs",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
- "version": "1.0.4",
6
+ "version": "1.0.6",
7
7
  "mcpName": "io.github.fboldo/openscad-mcp-server",
8
8
  "description": "An MCP server that renders PNG previews and STL geometry from OpenSCAD (SCAD) source code.",
9
9
  "repository": {
package/server.json CHANGED
@@ -8,13 +8,13 @@
8
8
  "url": "https://github.com/fboldo/openscad-mcp-server",
9
9
  "source": "github"
10
10
  },
11
- "version": "1.0.4",
11
+ "version": "1.0.6",
12
12
  "packages": [
13
13
  {
14
14
  "registryType": "npm",
15
15
  "registryBaseUrl": "https://registry.npmjs.org",
16
16
  "identifier": "openscad-mcp-server",
17
- "version": "1.0.4",
17
+ "version": "1.0.6",
18
18
  "transport": {
19
19
  "type": "stdio"
20
20
  }