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 +33 -1
- package/dist/index.mjs +83 -32
- package/package.json +1 -1
- package/server.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# OpenSCAD MCP Server
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/openscad-mcp-server)
|
|
4
|
+
[](https://www.npmjs.com/package/openscad-mcp-server)
|
|
4
5
|
[](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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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
|
-
|
|
255
|
-
|
|
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) =>
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
17
|
+
"version": "1.0.6",
|
|
18
18
|
"transport": {
|
|
19
19
|
"type": "stdio"
|
|
20
20
|
}
|