From bc580663fdf113dea3e84613a3773ed5210e1cea Mon Sep 17 00:00:00 2001 From: tomtang12 Date: Fri, 28 Aug 2026 18:04:25 +0800 Subject: [PATCH] Add makerjs-playground MCP server for AI-driven IModel development Adds tools/playground-mcp/: a stdio MCP server that gives an AI agent a headless edit -> render -> inspect -> fix loop for Maker.js playground IModel JavaScript, plus a browser sync path. - render_model: run IModel JS in a headless Maker.js sandbox (faithful to the playground's require-iframe execution); returns extents, path/model/ chain counts, captured console, and SVG/DXF/JSON/etc exports, or a structured error with line/column. - set_playground_code / get_playground_code: validate then publish code to docs/demos/js/mcp/current.js, loadable in the live playground via ?script=mcp/current. - playground_start/stop/status: manage a no-cache static http-server. - list_models / list_examples / get_example / makerjs_api: reference data. Includes test/selftest.mjs (28 checks: render engine, code-file round-trip, real MCP stdio handshake, live http reachability) and .mcp.json for Claude Code. Co-Authored-By: Claude Sonnet 5 --- .mcp.json | 11 + docs/demos/js/mcp/current.js | 123 ++ tools/playground-mcp/.gitignore | 3 + tools/playground-mcp/README.md | 99 ++ .../playground-mcp/docs/verify-playground.jpg | Bin 0 -> 82257 bytes tools/playground-mcp/lib/playgroundServer.mjs | 133 ++ tools/playground-mcp/lib/render.mjs | 281 +++ tools/playground-mcp/lib/state.mjs | 74 + tools/playground-mcp/package-lock.json | 1582 +++++++++++++++++ tools/playground-mcp/package.json | 24 + tools/playground-mcp/server.mjs | 206 +++ tools/playground-mcp/test/selftest.mjs | 188 ++ 12 files changed, 2724 insertions(+) create mode 100644 .mcp.json create mode 100644 docs/demos/js/mcp/current.js create mode 100644 tools/playground-mcp/.gitignore create mode 100644 tools/playground-mcp/README.md create mode 100644 tools/playground-mcp/docs/verify-playground.jpg create mode 100644 tools/playground-mcp/lib/playgroundServer.mjs create mode 100644 tools/playground-mcp/lib/render.mjs create mode 100644 tools/playground-mcp/lib/state.mjs create mode 100644 tools/playground-mcp/package-lock.json create mode 100644 tools/playground-mcp/package.json create mode 100644 tools/playground-mcp/server.mjs create mode 100644 tools/playground-mcp/test/selftest.mjs diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 000000000..f6d168d7e --- /dev/null +++ b/.mcp.json @@ -0,0 +1,11 @@ +{ + "mcpServers": { + "makerjs-playground": { + "command": "node", + "args": ["tools/playground-mcp/server.mjs"], + "env": { + "PLAYGROUND_PORT": "8020" + } + } + } +} diff --git a/docs/demos/js/mcp/current.js b/docs/demos/js/mcp/current.js new file mode 100644 index 000000000..b644bae6e --- /dev/null +++ b/docs/demos/js/mcp/current.js @@ -0,0 +1,123 @@ +var makerjs = require('makerjs'); + +function Wardrobe(totalW, totalH, baseH, topH, thk, depth, spacing) { + this.models = {}; + + var mainH = totalH - baseH - topH; + var halfW = totalW / 2; + + function makeRect(w, h, layer) { + var m = new makerjs.models.Rectangle(w, h); + if (layer) m.layer = layer; + return m; + } + + function makePanel(x, y, w, h, layer) { + var p = makeRect(w, h, layer); + return makerjs.model.move(p, [x, y]); + } + + var front = { models: {}, paths: {} }; + + front.models.base = makePanel(0, 0, totalW, baseH); + front.models.body = makePanel(0, baseH, totalW, mainH); + front.models.top = makePanel(0, baseH + mainH, totalW, topH); + + front.paths.splitLine = new makerjs.paths.Line([halfW, 0], [halfW, totalH]); + + var handleH = 100; + var handleW = 6; + var handleOff = 12; + + front.models.hLeft = makePanel(halfW - handleOff - handleW, baseH, handleW, handleH); + front.models.hRight = makePanel(halfW + handleOff, baseH, handleW, handleH); + front.models.rightTrim = makePanel(halfW + handleOff, baseH + handleH - 15, halfW - handleOff, 15); + front.models.rightPanel = makePanel(halfW + handleOff, baseH + handleH - 30, halfW - handleOff, 15); + + this.models.frontElevation = front; + + var internal = { models: {}, paths: {} }; + var ix = totalW + spacing; + + internal.models.base = makePanel(ix, 0, totalW, baseH); + + internal.models.topOuter = makePanel(ix, baseH + mainH, totalW, topH); + internal.models.topDiv = makePanel(ix + halfW - thk / 2, baseH + mainH, thk, topH); + + internal.models.mainOuter = makePanel(ix, baseH, totalW, mainH); + internal.models.mainDiv = makePanel(ix + halfW - thk / 2, baseH, thk, mainH); + + var rightInnerW = halfW - thk * 1.5; + var subBayW = rightInnerW / 2; + var drawerH = 18; + var drawerAreaH = drawerH * 3 + thk * 3; + + internal.models.subDiv = makePanel(ix + halfW + subBayW, baseH, thk, drawerAreaH); + + for (var i = 0; i < 3; i++) { + var dy = baseH + (i + 1) * (drawerH + thk); + internal.models['drawerShelf_' + i] = makePanel(ix + halfW + subBayW, dy, subBayW + thk / 2, thk); + } + + var rodMargin = 3; + var topRodY = baseH + mainH - 12; + var midRodY = baseH + drawerAreaH + 5; + + function addRod(name, x, y, len) { + var rod = { + layer: 'rod', + paths: { + top: new makerjs.paths.Line([x, y + 1.5], [x + len, y + 1.5]), + bot: new makerjs.paths.Line([x, y - 1.5], [x + len, y - 1.5]), + left: new makerjs.paths.Line([x, y - 1.5], [x, y + 1.5]), + right: new makerjs.paths.Line([x + len, y - 1.5], [x + len, y + 1.5]) + } + }; + internal.models[name] = rod; + } + + addRod('rodL_top', ix + thk + rodMargin, topRodY, halfW - thk * 1.5 - rodMargin * 2); + addRod('rodL_mid', ix + thk + rodMargin, midRodY, halfW - thk * 1.5 - rodMargin * 2); + + addRod('rodR_top', ix + halfW + thk / 2 + rodMargin, topRodY, halfW - thk * 1.5 - rodMargin * 2); + addRod('rodR_mid', ix + halfW + thk / 2 + rodMargin, midRodY, subBayW - rodMargin * 2); + + this.models.internalElevation = internal; + + var sx1 = ix + totalW + spacing; + var sx2 = sx1 + depth + spacing * 0.8; + + function makeSideSection(xOffset, isRightBay) { + var side = { models: {} }; + + side.models.base = makePanel(xOffset, 0, depth, baseH); + side.models.main = makePanel(xOffset, baseH, depth, mainH); + side.models.top = makePanel(xOffset, baseH + mainH, depth, topH); + + if (isRightBay) { + for (var j = 1; j <= 3; j++) { + var shelfY = baseH + j * (drawerH + thk); + side.models['sideShelf_' + j] = makePanel(xOffset + thk, shelfY, depth - thk * 2, thk); + } + } + + return side; + } + + this.models.sideSectionLeft = makeSideSection(sx1, false); + this.models.sideSectionRight = makeSideSection(sx2, true); + + this.notes = '# Wardrobe elevations\nFront / internal / side-section views. Tested via makerjs-playground MCP render_model: 117 paths, 33 models, 28 chains, 614 x 271.5.'; +} + +Wardrobe.metaParameters = [ + { title: "Total Width (W)", type: "range", min: 120, max: 300, value: 200 }, + { title: "Total Height (H)", type: "range", min: 200, max: 300, value: 271.5 }, + { title: "Base Height", type: "range", min: 5, max: 15, value: 10 }, + { title: "Top Box Height", type: "range", min: 30, max: 70, value: 48.5 }, + { title: "Board Thickness", type: "range", min: 1.2, max: 2.5, step: 0.1, value: 1.8 }, + { title: "Cabinet Depth", type: "range", min: 40, max: 70, value: 58 }, + { title: "View Spacing", type: "range", min: 10, max: 60, value: 35 } +]; + +module.exports = Wardrobe; \ No newline at end of file diff --git a/tools/playground-mcp/.gitignore b/tools/playground-mcp/.gitignore new file mode 100644 index 000000000..8b85bafee --- /dev/null +++ b/tools/playground-mcp/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +workspace/ +*.log diff --git a/tools/playground-mcp/README.md b/tools/playground-mcp/README.md new file mode 100644 index 000000000..7019e130c --- /dev/null +++ b/tools/playground-mcp/README.md @@ -0,0 +1,99 @@ +# makerjs-playground MCP server + +An [MCP](https://modelcontextprotocol.io) server that gives an AI agent (e.g. Claude Code) +a fast **edit → render → inspect → fix** loop for **Maker.js "IModel" JavaScript** — the +code in the right-hand editor of the [Maker.js playground](https://maker.js.org/playground/). + +## Why + +Iterating on playground code normally means: type code in a browser, click *Run*, eyeball +the SVG, guess what broke. This server lets the agent do the same loop headlessly and +deterministically (real `makerjs` in Node, structured errors with line/column), then push +a finished drawing into the live browser playground for a human to look at. + +## Install + +```sh +cd tools/playground-mcp +npm install +npm run selftest # 27 checks, exits 0 on success +``` + +## Register with Claude Code + +`.mcp.json` (already in the repo root and in the parent working dir): + +```json +{ + "mcpServers": { + "makerjs-playground": { + "command": "node", + "args": ["tools/playground-mcp/server.mjs"], + "env": { "PLAYGROUND_PORT": "8020" } + } + } +} +``` + +Then in Claude Code: `/mcp` to confirm `makerjs-playground` is connected. + +## Tools + +| Tool | What it does | +|---|---| +| `render_model` | Run IModel JS headlessly. Returns `{ok, kind, extents, stats:{pathCount,modelCount,chainCount}, console, outputs:{svg,…}}` or `{ok:false, phase, error:{name,message,line,column,stack}}`. Params: `code`, `params?`, `exports?` (`svg dxf json pathdata openjscad stl`), `svgOptions?`. **The inner dev loop — no browser involved.** | +| `set_playground_code` | Validate via render, then write to `docs/demos/js/mcp/current.js`. Returns `mcpUrl`. Refuses broken code unless `force:true`. | +| `get_playground_code` | Read that file back. | +| `playground_start` / `playground_stop` / `playground_status` | Manage a static `http-server` (`-c-1`, no cache) for the repo. | +| `list_models` | Every `makerjs.models.*` constructor + its `metaParameters` + defaults. | +| `list_examples` / `get_example` | Browse / fetch `docs/demos/js/*.js` as reference. | +| `makerjs_api` | Shallow index of the `makerjs` module (namespaces, member names, arity). | + +## Browser sync + +`set_playground_code` writes `docs/demos/js/mcp/current.js`. Open + +``` +http://localhost:8020/docs/playground/?script=mcp/current +``` + +The playground fetches that file into the editor and runs it. Reload the page after each +`set_playground_code` to see the update (the server sends `Cache-Control` off, so a plain +reload is enough). + +## IModel code shape + +```js +var makerjs = require('makerjs'); + +// (a) constructor style — `this` is the model +this.paths = { c: new makerjs.paths.Circle([0,0], 25) }; +this.models = { r: new makerjs.models.Rectangle(50, 20) }; +this.notes = '# markdown notes'; +``` + +or + +```js +var makerjs = require('makerjs'); +function widget(w, h) { this.paths = makerjs.model.originate({ /* … */ }); } +widget.metaParameters = [ + { title: 'width', type: 'range', min: 10, max: 100, value: 50 }, + { title: 'height', type: 'range', min: 10, max: 100, value: 20 } +]; +module.exports = widget; // a "kit" — render_model constructs it from metaParameters (or your `params`) +``` + +`require()` only resolves `'makerjs'` (and built-in model names). + +## Layout + +``` +tools/playground-mcp/ + server.mjs MCP server (stdio), 10 tools + lib/render.mjs headless Maker.js execution + measure + export + error locating + lib/playgroundServer.mjs http-server child-process control + lib/state.mjs shared code file + demo browsing + test/selftest.mjs 27-check automated verification +docs/demos/js/mcp/current.js the code the browser playground loads via ?script=mcp/current +``` diff --git a/tools/playground-mcp/docs/verify-playground.jpg b/tools/playground-mcp/docs/verify-playground.jpg new file mode 100644 index 0000000000000000000000000000000000000000..4403593c24e5779974c198b038ed23c5cc6b2bf2 GIT binary patch literal 82257 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%0A*3Wv5P~=H?e-G(=Y? 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

Fb>`8h z?n=VhE-_q_Dwd_P1zEm{7cF${o)avbwta(Eq@*leiZ}H@ zHDpGPL_h4La)>6~hMSQtS9sPVzbsnIe zgDc4W0ixVP^Ne*w>)W@VQws+!mNxe`RK*o0qnib-77L{K)$$9^i&)k7<%7 z&2Bu$V4_?J8PHrjeFlm|r*=#j$cl>QL~;_JyoBSFINvdMk6=bDCV^^*CdqKLzUH@a zHJT*peZd+@R9+JCm;!1k<}ZoK5YVQ=hr z_q{<0u@Dw>9v-_?GmFmm^fW$WkP0mwVw3O_dszJoX6O`omg4OzmQP2h1c74}1Hc?y zd9W88t|et`SzWA`-~I{>mrt|rS28E1 z2+vyy*gOYM=A4E#!|=3;)C7@i-$)`Cwk5YT14V^Eyebi6IV3X>qr)_gh+qt-0-!n-QOz#F1oAmj8)_0mb z@b+s>(r@#T+O@Kl5JHy4udr`gA=3r(UC-i^v?ZMtIVrXKu!zqGlF-LMGa^L1)6AV1 z87KS0+e5A6_tVvcpzBLoc%23PEO+v>)y#?2jT_E)v_ic z7mCt_SO8pW>HYUbQok8+qRkuv>DCAZp-XBu+x594!fv0VwRzu)>ap;VXtDP-Ywe}E z;{0nRr|yd;iQOJ@74vDJo0ahI+|+!KW8ZNciOq0bu{a(m%^6j?;`&Y`j9u}NgF|(Y zCebf$+KW-R8dZDx;vFY*Lbd3<<&uaUcPjcWOa@D!#b!oU=1` z&(}`XWwK06ol+NLI7H0cstdkLurjJmzJcTA+n%uaBkNK1Qv1MX=` zkdX9=drt^%*cec4TXT$6VREK+{q=7v*t!*V>^{tIEY{d1@MnwpoaBikhv!9glp9pR z;Bs3D)~D2#WCd3IQBw8ZMGXR1#0(`%@l3(Zz4QW4s7J`9j}%ruz=ocZMv9k@ve4(dKDp@n%2OUnfx^Gly& zcWe_X9kO>QND=Dl5b$bMtwzr}Tp!ly<;(& zu(4^%5SGpHtlL<|p6!}b;?DeyEZ8+>m`5w&x8-&jFumHWSngKWCVWsJ{7Y-X*ueb7 z@$a$Ek{>Skj3NWEA`-xMfT0--C+Uj5jbQ&id{U@AG{fxFwD1&Too?Z&6Dv8$7xk)K< zJ>6XRS|p2uD{V=J=mLsQMNdPHGk)4Mp_wWu!15>76|C5ZDN=LKKWoyse$;0sz}otQ z{TznsVRlgncMb#DGOUlavG;KqdHOxHQFh{oRY`9+S6#ICcs*{)>>^+;>3(sO&>({gccsH7wI=7C7#)%SBHYDa}_9U>ALYt zpbwgs!z?BW%c2KliqjABK0c82lozplAw6H_`CN}FHPEVhuvyRP3j#6n>dp1hwzf;; z4hjaHY#j7vW*yR+xeW7b-s)%_hfgA5hkfyrjs1q}{*^%=F>ECfG6_B!phV0;@5U-N zQ|8_TfJ#^|@o|DM!YspsHzi+RAgbH`&r{~s?(Z~vbp(wYc(k#(p)U!oGjW0$@(Gbg z)O_{ff7U^IAe}8kylF^jdd`=lKF4e28z$+EQ99rHjV+#O;Q#qF=^bmE#`POF_R|jP z+zxX(e31=`QBN%Ou-La|vqMedQnwvHrqz>p>0nyJmVTS*Bo6j%ug2al(epx_%K~A< zM9pYne>HsxeK{%FFG-Q+GE!BSmK8a~KviSqsz1DXVmNuJL;m6_zA5-*!rp512x3&Tb+y+GO!p3#-*)(M22@|Z6XT3GBZC&l4yicEIK-5Z2|MxwIYPffQh z-RfdpS0i@wT|0Mx4=9CugneKtdpUM&urV}wXXWj^JrP8%@*@2IQik>a02};A;(Oyy z4VX-zidk8WG%$OHb($AfrlJ+6)eUd54Op*t=Lo+O&ec5GSE^FKQpCdP>oIwa!F`u#AIb4O!x5m&}$9c7;ON=^J!W+0^%VGdx_4v=OQ*CbiO>&~yk;)-or+ zv|H~wKKxe6TjAwst`}9(MG+ltDs3KH_%PYPbN6T4r z0C~FzW+PGwPI^Byxi#XD69~ZV<)7_VB%6+F%p6Hy8}rj-_p(C1AD*~yuj*(1YV!L< z>&OM^qr+6=cUNa=PzQOmM{f^mz4K7fF$R4xt#pLHbzMD#^Au1w5grc6KKpu&d_UPm z*RS_({Z%)&)vu%5Hj;^*$~hBE#0M z2uf>rb4Yn-^TzK;*v4q+i5wNWFW-)STVCnRi2`LJcJ{whO{4u@pv+ zdP{>I8&SRiu9(;0#ypYewSqZ!-u4k7XlnBr3b;9W50%4n6GwP&byv%O`sF(?*S33B zS(01_(b=GRqqx(OBUvLXS~_`o{<}d_qLolhNpkr>e#xODYpoOfrD9LOp2x_DyHC;8 z?1BNqQ5%f+gH=a!Gv=n0^f~pVbv5@m%U{-aMTygvWul15suBw1Mo;(6bB%^CnR~cB z*t7;;IbQ0@^JgZ;veII*&SEh3ks`H0&xW=aBPPcUgPw;pfp^DO2HrE{&Zlg;!p3h% z{QUE|S$E4%T{yQl(#{J7p!UH9-#8~mP`?%1u#H;WLzc3-tFr>tC;R%K?6(ru~cF67# zPvtxn;n@mM;-jnry(Ql#6Kaa32b7Pqdq14dJ8ufu@MD8YxIKklF{fP2|RQBEq?ic3Kq={K&_4Pepm3V^Y zY-Yll_mR7mESt$!ZHW?yd>7}`f?{*rV(tSLMM7|5pLD9N7Xk!k zi*gER?`Bsd{kWDfC;;ymQ7yU+3jxSZFS8DXE#6QTtGrPPYN%6u>!7D0EJ#~`&wv`N zT%?x6JGUfwHx(9M;kbO=SMc+G0zm&S!L)^Ua__s%f|PXwB8FvG;pS#z?zyLKJz;js*KVVNRa&O((({ ze9<2SK6d#U>1?d|CoDt~((lr+x-T{-p!e@gI8`Cwo~mO_Dg-A_=cq(oA<|}J$ALhj zrXJl(e`z(grDzp;gB!PDGm<{NYJ}EmxZe@V-Sa~bVW2n&k$sQYCfL-T;of@k9cscJ zUCyQIV{jSe`~fLNPw3z!SRsqNS1oPI{H!v31wkg>-?$wFX%6R8*RTy^!c~)p0vJs_ ziLKRqc0C+ESvV~*_l>JwTotnFJAPkMn=eWx(*?2QYc|c&&x{HNwN0V(8#tmXih|+E zQVH2b_D;C*Z;>sHpdYk_9cR}4Qq)iR0*gaPe6>_L3iVOSQnjMQ&c1mycwA)qix`Pp zk=j~) z>X2s-cRqyn^O=&A7bNYREImK-8U$%O0fBEGBz+icK{4m`f|H&pj#oP5o>ovZt`^q` ztjmQR3pmLWaJaw55k)?ZDGfuXJUk9`-61ZDxl~H7aBjF{*ysP6Sz%jmCh!k4CUdNsl=(A<(GFz}c|(@dzW{PVo9 zKY$g2IFQ}8Q)ff6C*?rlZC2DrT`QJp5;O&}6K z)n#5qja*w-gOBnKF9xy(YsRJ-k#6kU zq7&r?r<8sT*~*Psoqyi~EvF?%vGc4V-Nte~%vE<2(18irBVI4ll)rW2vUQ8%wbpx2 zbopI$*6^(9O1q4&r*I?#w zvcE6Rb*=3%a0ASxt1jq|P|?6lTJ_Grul@ku7t}p+Z5A}icB2`mQ?MrM@J{S>FsdYi zN1C&HDyEJ=`o(F9n9{ed)C2K%dBc4#xaU;Jxq|{iWOl5X`f`{sZ`IFi1wKDXe5*j- zr;pK(0H9&(PM=h}9dG|2))tnvd$Kdor-h@x?_8F0w!igd3WH;6%ey&F9{{2G4z{tp zee86SqD{xX^DcYEhJgLEI=a28_&{{(68s~VrU{XQiaoeCQESt3u>jLN;2z|l!OrrS zPa|N-r(J|6j6pavyd26Q;(ds_5-!~7l$pVo*XtYtMCE-pF&@@?dVL2?Hx`cpI)UI3 z#VMZ0N1NXsC(w@wmHo)ui??yf!_X1+YLX^n@b#LFo3`xW4pHwkjn=XkywvpogTb8# zPT$YEX-6O&%IUQmC4o9rU5vZF(1d``Gans0@^{qgq?LS)8`vicE?HP;Mh^t9FGD}r z-phK!T7OG{F(E-J%y$Mh9|^y5k$%oXan)Aa+)$grzq`f6I3y%m_dNcK)I;4y zy}E?i9|L-tB0k72cQYdPFt_Zsf7_GG^A`qpu28XH169YN(r{N1YAH|Q+tmKG@_ZJ4 z!)AvfZ?j0A`(al?84*$T6H^nr@DMX-VSmhbF9c6=CSF=0bJBX0zvD8&6?d+rE&aK{ z_;n_8>~q9bpY+K{&t3Sihu!EC%clHTdZNA*cF3PmF!$HrfWna=sRJbkAT>9Dw4p4abPvDnU5SV3z^rG7yui%B=g6|;bBhe3$7Ut|zMXBCn)5~*5)<~N>325vp*m-M5u zgB9$cJc(+!AjX8O@Ym*--9Gup6}L4$B>@*)1L=YWLz)FP@pRL%YvpW|NJt^qdk6Ju zL9RWW#tsiPKY1m{qlv@xvGsrm3}J|QILrr&op07)*Gf*;YS^(yAw0d%Au8>X{v|BP zI0xFfSu+bczSTTo_1i+WSE&-1xVq8VU>Cb5${g-OR-)rowTSPt9$Vq>zZq-k@5O}F z+CZ!xP{BCUq~cqY^epAUeW%raUWnHOkGx2W0AWNZN>B5wV`=GHHxD1r@mLggNV-{E z;F6@|to(O@`RxcuUmd+lltL|C*V1;vLIdzEr0+3WRGnz@j&dck~7e#r3%m_16)09X~!>_2;I_t?SAa>8;= z;}}y@A8u(tTnqKM){(=IEX-Dn&g)Lur8cM}r-zng((c{G3*h2J`=I6m_Is?)>}^!4>+-aV8L3PF~-fhw{xbp&p3>yN;EKEwVEgO-$*s z?dx*$iWpCdQl0s`>!R|yxxJUAod)a&pzl6?A$Z%3^xY6H$yDfFv;_v3TS(3-1^j;D z&BGI8)F!6GCu+K?)b-8K!Ojkr^W*l9j(1KCq(hmNej#5txwUTUS5q2FF!_!a74P9! zS*^d!re*08;S4;DN7w44`jn3^3`Q+CFM6GNAYc^0%r3J1wR|7_QYU~llnWIlh)LAT zz1uXs`{CDD>GVjeVa@ExXq4o_RUM^-ZlqKCon>Q2qdZdoaf3RFyha=YKvXzp_e^4p@IDS z_c0V7aX&X>MnM@eLcz0h&;3fjj~g{#=+f~N>fRHe{?LE7@4%eu#|}IaqA8h7pH2s% zKmaKpE=@&5Km?r1y`?pd^NJ>V%$yVh8t0A=B+n70jQulc?B6m#H^N?a0=cPsy zb^()8y5BE#yc63B_llg^+pkne<#J$#y{muao6$NXaMIVr?2urzE9xSi86Jai6D1Pc zc}^dgzg*MD5N|t^HW@u=8rWH8W|cNa{sv6C%gV01`L>PP$oQfMJ`hHa`ER9cECe2< zC+_^x!>;g>8>Kczd!GD3+={=$r^IlhI$pf|IR7u&HjI;}T2F`E~TRZxkDpz{=o`f-~db>}C5ZCIu zR9IxBJ`cJ#O-&tH!QNd@)Teox#Nd#>2~@9fKq#s|{xeW$d0)nGW3j;<#oKXm>Bz;; z@+`j&hvY`_m^n0S1PXrG^B&t%E6uLc{m2K*9v*4<$`k+w1dy4!rDGt&dd_WH*uhIf z{f6Ptxz5)2R`AkDMg4fEd=v~d8z{NOYruxTTfzaQ@~^Q938LmxZAsrIpEI+ z;F*XR~@Kg7HcH1k{Y~VB6Y>a#&XR?RD^7hw5c7 zsBlg&U`-X^8gABA=uOZtN_U3o3Yl;>kbKA>8+JtIjl>vPrMt&npwn()B< zL_Zs^X;ns)HqCRCa723ug#}Ipu;fYDV&>6Nrz1@kfx>5GfOA8Xv>JnxRPL-8Y_mbV zPXGEUh;P^p+qnM~>}QWL6j&K~;mm{0k>-xVO>FBgsi8w3)$wjW=>Ts%t?wNvsfN|r0FU$6}%!&kOjHiM#CYs7vFYHAzV zwFDn>C`y&921zGM&x2teZF?mp6MDHPDt)ZrL1fbLs-G#>&fQl|jnO(*e!5g#M@e*4;^YCA8(_Nd_l{fQygQSYe3?zV zf!XeUd*V{)CPi^R^P~<)LlebKg!w>t-{KtQo*+AZNn3%AsE%S$6WGT#+}Ff@eHyFy zVHxb2R!uvEL=xBuA~VG%*T1cTU~$>%;khRYGneF3nz}ZdO}Gsu*tA^K@5Qh6{{kxY zKw+;^sv531b`?4F)gQ_$j5LBl058?Dq?$$K@yvoPVPF(o@Ghq%u0RtyysRMg6g{=%}eeGuYS#e@T8K z1g;&dsR||rtOaIoiVCe(^W0VY9Jai-agr=D)_j1I5MN@ zb`BRTw8nQQD^9n92C zf4EEDtDpC?=(W?tKp_33DhCT*vz?jzz$oAg!@X1KBLL3=fX0TWlhS2cvE#Zx6A(xSf9bES&QZ+aymMq<8{>Iy!oN|KY`|* ztI8`Ohq?PNd`~=n2EVZDoWDNOIz}J;9k32r8{HQ=_r37?js6du`hTPf$Nx`+_+L!& z{^c`YBtjG{8vZ3uziFGz>5$=i*uk%Z%UFB9z4Ft4%Fph5i?gEj?1x(PgRwxLuo5%> z%p^j@T0V28S*F>uZvQ`KOG`zIA1h!^g6qgqe7 zp0nlQCd_#L)GwWUmsk=C)?#~R6=S5e-uDdpRA=oi+EV|*1s#+bWNdrXr>B3yA5OVe1!?%)uaQ87Lqu zukL9ID85EsWN#H_=Xlfe{Sb`Xa*6r@K`irbR1|xcc?_nWbh@=t)_i+8d76vnx!}Rk z%(l;9kI2W8GN%jc3uBS*$x3Z9w~jTWf3?v|=%EUJy<{{@xfZRb2X@X`ZPwEZSqJQM z?_0qZJukGTkfa-<<6mXd~>XOHJ|J^SINQWh<5Sv%Z+_a)2b#+aZM&`SoJ_nD;I0;KpCoKF(6Y8eCA8 zq?uEKhO2rsH*g=aZRLY~Tl%5YHVEN}t`Frves$-dC*GY?n~Ke#xE;2(w;ytzC_x(5 zB6}8L=a=TBF5eMlED2(^%O&2lU$iClAivHn(q`XzzBC&9^j>l<;R_WBV9j)Pf1i8P zBdqenn)mmF;E7w3G$HEYx_sJx3G@%|D7|Rvbv<%%B)D}AftkU#C}&#j7JqEnN20&B zb|^TNrtb^rldpA1b689^ZjKG5Bym-ZAfG%lxJXK8-R8OF{y;Ol<~@N-o-eGiE!(C5fbH0Sf9#!|xsJKUA=K5nEuN;#@|u6MC@A4c?U0p)HbYhs?XYepaggu&^-p zN#CLfk;2h23USO~&%a0}5ghYEa+H7Ea4MG?*VrsdHp{gjt_mc`wiPG7{`qK?v+{`e zO$cUT@139&o+=xT-#}3xmDA1&uw^lEeO3Jf-8W*F& zo#2GaPVJBsaNc)z@d~c!ue~P_8S0h#p)T3A_=e3K>)`3gKR$1McI~1S9UScy_2S)w zLj=qKx+ovfKVbW?O?KAaNp3w$ri5H7x}Ji{VOFEB_qw;^meYequ!OtHy&T*}RmtS9 zT8ETO-=x;vK`-LX@ih*P?e$+taGYlQ6s7sHkFgD_KYi)~_rTmd=i76#szlQGk&>_^ zium~~d6Rc6y}OG~#*Dc(C@@xN^M(_@pq!SV{(`ul9cDidbRNuNyE%RjDHmZLGd&I9&6TD%O3XcXdPbau5z^tQ=QCn;Tk zzr&0L`PzopyNH_Cnf}ApLQ5Y$ddm*07yz!ZFz(&_$jugLgTzL*Z0BXhS69FTANkEa z7|c-{T3c;HR__H*6NW7cPmk^8u7BTsWxsP_7^kDg)NH9cKFB?RiXf972ILD%kF_7M zpv)FCdv#i;UCiRXThUhB`D}e`i!o{Q&l3*U@*GKD&pZtIMQqc2kFPRGly-)&j4&iE zgroWeLLEx?K5+w!Else-2G2yU!qj4y9?~5?7M=PITLvcD*CR??C;H7cwTSmnA=49i z0XuYi$T>C7*uosviVv)ErNB0$nFV?vT?(N_>7rs_+$AI;@>C`MvaoOxI;b^ED)%j( zF7|7@pgBB2-l-$@ase$SCQib}{H(g%s>3>dZ<|aMCqGtI=AWq&8nR{bte;|lAXR=Z z2()P0*gONtb~J1qw zr7Z3d-qg_yIMG*j)eebqwa}kQ9!+@wggw*^y~d2ikD1PBkSX4iTA0)~@jl z&o%VL7_Lc?l5cl9+I;~CmM&79&o0VT4&KV*k`(>tJXp`CBi|mOW4Y`dkMI`}$_%J}!H-~} z%JO^o+LP9i);%!0Vr3KGGoYyVt*&0O3kaqQX(gnu_OM-v*SF!vw_5y37?FvJS{zGL z5|(%+J|8@@nbg?!G;rQeDbuZH4YlDD)Tpf~C0I|c{8v-P3t zSnIT!O1p6bX|y_(aadzZR_rnO6{=S6-2VHNC-b+&i!g{RK2ny$qjs*ybtb|8)}T+N z#pU_AwQci)E=&|?YR+(1UeYN{vFaTpr*cVUz&?tFoS_f`Ac$qrp@r$ye|ss9<~{;a zr7c`cFtwQgjgR`d@h*T`{3ZCz4ob!?7)<+p18TWeYc<5gacO1RnD8&BX}o)}{|L#c zB=YO|%jf?B?BDa&J+A&<#u{Gu80%N!Ej~1JD#J)_gKoR0ax291T?_u$Dj{a{aDmE= zeFI6eL5EY?;~}qByDf)?jr9xbrHKIDkL>XT*f!4oPEVP_|wKT<`O^!K><e!5|YZV+_>&yZ7FI zcrx=;r*qwu zttFs+L#&r(epRv?89x>%ht>YdvI@osD{_VQzYJlYC*9Z*D7;=*SEDR0-JqLS7e^mC zH3?Yboj$n$A&bi$rD_*Rft-iFU{L>pX0;A?5_z8cOhoOdx-;Kl^$Eg}G5XSRqY>qr z=AO4uXIFCtR8D*@RD)(>j6z>gYgI$P?v!0Cs-T;f3OB2qZ~{8wzYMUtLK z(6dp6bPSlXTt7!4t<*Tj^cQ)QsveI#UGUf!#iv#A)O|47}^Y8dIS(q26 ztF?rH0kw2%=Pm{-A5GLA-rlm@$o+kPH}=Vz#(Gp_91DE8d<}waurOCQGQUp_t-kB+ z&RBoZnO*avf`z-I$j?WGuI9?hi9X6ey3vilWfCOuoKV{ZUaj8ZPu*)2M45~|Xc1Wx;d?mKHutQ7@ji@6NhP@ zuuR(+e?yEHMhzRTtvCoKBx_B~QTO#yi7rg1c?Rq*n?{GDA8D)paKyfalVIw=0T0;R z3X}ZNEjc+2WTV@Y;tVdrbZ zKR$HHbqh`r2ZS|UVd+F*44$Lkoi2~#&IpUAy0*-nY-@)}ty2*@nzdV$;W#%~zE3>f zJNemtt=1}dO+)yy^x)Vh-_y&c;^{ZQ9qW@C(_K4BT0K%-tFMf`m~-C#4Y-tbuSg(i zn%h@_NW_Gj;<_>*$Lut21dGBees*4&lj+>fN#J4a?45WsAOKpu{hPnAWWwZavOXz1 ztw9u;d`ey8jE8#S*HwE*poLe?com+SsP4wnVMiLAZ80=J)@J1Vr>;uYYt9S*6L9h$ zaFqYu)&GyT{O_^T|B=M>|JP;!7uEx(hHUmfhi*Klzm66g8+69)fECZ=PdUafq=#%i z_HZr*aV!TY5G@i)4JhZPtIHD8uXP?zep!0m(4w_*>$(*<{3G+p&;_62fEM~}kI*9A evek1H()A%oMrHB2$*&5J3%k|-_Yxj|PyRP3MB}Oe literal 0 HcmV?d00001 diff --git a/tools/playground-mcp/lib/playgroundServer.mjs b/tools/playground-mcp/lib/playgroundServer.mjs new file mode 100644 index 000000000..ccaae29ef --- /dev/null +++ b/tools/playground-mcp/lib/playgroundServer.mjs @@ -0,0 +1,133 @@ +// Controls a static http-server child process that serves the Maker.js repo so +// the playground is reachable at http://localhost:/docs/playground/ +// +// Caching is disabled (-c-1) so that code written by set_playground_code shows +// up on a simple browser reload. + +import { spawn } from 'node:child_process'; +import { createRequire } from 'node:module'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const require = createRequire(import.meta.url); +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +// tools/playground-mcp/lib -> repo root +export const REPO_ROOT = path.resolve(__dirname, '..', '..', '..'); +export const PLAYGROUND_PATH = '/docs/playground/'; +// The playground resolves ?script= against MakerJsPlayground.relativePath, +// which docs/playground/index.html sets to '../demos/js/'. So script id +// 'mcp/current' -> docs/demos/js/mcp/current.js (served at /docs/demos/js/mcp/current.js). +export const MCP_SCRIPT_ID = 'mcp/current'; +export const MCP_CODE_FILE = path.join(REPO_ROOT, 'docs', 'demos', 'js', 'mcp', 'current.js'); +export const MCP_CODE_URL_PATH = '/docs/demos/js/mcp/current.js'; + +const DEFAULT_PORT = Number(process.env.PLAYGROUND_PORT || 8020); + +let child = null; +let currentPort = null; +let startedAt = null; +let lastStdout = ''; + +function httpServerBin() { + // resolve the JS entrypoint so we can run it with the current node (Windows-safe) + return require.resolve('http-server/bin/http-server'); +} + +export function playgroundUrl(port = currentPort || DEFAULT_PORT, scriptId) { + const base = `http://localhost:${port}${PLAYGROUND_PATH}`; + // script ids are simple path-like tokens (e.g. "mcp/current"); keep "/" readable + return scriptId ? `${base}?script=${encodeURIComponent(scriptId).replace(/%2F/gi, '/')}` : base; +} + +export async function isPortAlive(port) { + try { + const res = await fetch(`http://localhost:${port}${PLAYGROUND_PATH}`, { + signal: AbortSignal.timeout(1500), + }); + return res.ok; + } catch { + return false; + } +} + +export async function startPlayground({ port = DEFAULT_PORT } = {}) { + if (child && !child.killed) { + return { + alreadyRunning: true, + pid: child.pid, + port: currentPort, + url: playgroundUrl(currentPort), + mcpUrl: playgroundUrl(currentPort, MCP_SCRIPT_ID), + }; + } + + // Someone else may already be serving it. + if (await isPortAlive(port)) { + currentPort = port; + startedAt = Date.now(); + return { + alreadyRunning: true, + external: true, + port, + url: playgroundUrl(port), + mcpUrl: playgroundUrl(port, MCP_SCRIPT_ID), + }; + } + + const args = [httpServerBin(), REPO_ROOT, '-p', String(port), '-c-1', '--silent']; + child = spawn(process.execPath, args, { + cwd: REPO_ROOT, + stdio: ['ignore', 'pipe', 'pipe'], + windowsHide: true, + }); + currentPort = port; + startedAt = Date.now(); + lastStdout = ''; + child.stdout.on('data', d => { lastStdout = (lastStdout + d).slice(-2000); }); + child.stderr.on('data', d => { lastStdout = (lastStdout + d).slice(-2000); }); + child.on('exit', () => { child = null; currentPort = null; startedAt = null; }); + + // wait for it to answer + const deadline = Date.now() + 8000; + while (Date.now() < deadline) { + if (await isPortAlive(port)) { + return { + started: true, + pid: child.pid, + port, + url: playgroundUrl(port), + mcpUrl: playgroundUrl(port, MCP_SCRIPT_ID), + serves: REPO_ROOT, + }; + } + await new Promise(r => setTimeout(r, 200)); + } + throw new Error(`http-server did not come up on port ${port} within 8s. Output:\n${lastStdout}`); +} + +export function stopPlayground() { + if (!child || child.killed) return { stopped: false, reason: 'not running (or started externally)' }; + const pid = child.pid; + child.kill(); + child = null; + currentPort = null; + startedAt = null; + return { stopped: true, pid }; +} + +export async function playgroundStatus() { + const managed = !!(child && !child.killed); + const port = currentPort || DEFAULT_PORT; + const alive = await isPortAlive(port); + return { + running: alive, + managedByThisServer: managed, + pid: managed ? child.pid : null, + port, + uptimeSeconds: startedAt ? Math.round((Date.now() - startedAt) / 1000) : null, + url: playgroundUrl(port), + mcpUrl: playgroundUrl(port, MCP_SCRIPT_ID), + repoRoot: REPO_ROOT, + }; +} diff --git a/tools/playground-mcp/lib/render.mjs b/tools/playground-mcp/lib/render.mjs new file mode 100644 index 000000000..c9125427e --- /dev/null +++ b/tools/playground-mcp/lib/render.mjs @@ -0,0 +1,281 @@ +// Headless Maker.js execution engine. +// +// Faithfully replicates how the Maker.js playground runs the code from its +// right-hand "JavaScript code editor" (see docs/playground/js/require-iframe.js): +// +// var Fn = new Function('require','module','document','console','alert','playgroundRender', code); +// var result = new Fn(...); // called with `new`, so `this` is the model instance +// return module.exports || result; +// +// then (docs/playground/js/playground.js -> processResult): +// - typeof result === 'function' -> it is a "kit" (constructor); construct with metaParameters +// - makerjs.isModel(result) -> use directly as an IModel +// +// This module adds measurement, chain analysis, multi-format export and +// structured error reporting (with line/column) on top of that. + +import vm from 'node:vm'; +import { createRequire } from 'node:module'; + +const require = createRequire(import.meta.url); +const makerjs = require('makerjs'); + +export { makerjs }; + +const USER_FILENAME = 'playground-imodel.js'; + +// Modules the sandboxed code is allowed to require. 'makerjs' is the only one +// the playground itself guarantees; a few common companions are whitelisted. +const REQUIRE_WHITELIST = new Set(['makerjs', './../target/js/node.maker.js']); + +function makeSandboxRequire(extraModules = {}) { + return function sandboxRequire(id) { + if (id === 'makerjs' || id === './../target/js/node.maker.js') return makerjs; + if (id in extraModules) return extraModules[id]; + if (id in makerjs.models) return makerjs.models[id]; + throw new Error( + `require('${id}') is not available in the playground sandbox. ` + + `Allowed: 'makerjs'${Object.keys(extraModules).length ? ", " + Object.keys(extraModules).map(m => `'${m}'`).join(', ') : ''}.` + ); + }; +} + +// Parse "at ... playground-imodel.js:LINE:COL" out of an error stack. +function locateInStack(stack) { + if (!stack) return {}; + const fn = USER_FILENAME.replace(/\./g, '\\.'); + // runtime frame: "... playground-imodel.js:LINE:COL" + const withCol = new RegExp(`${fn}:(\\d+):(\\d+)`).exec(stack); + if (withCol) return { line: Number(withCol[1]), column: Number(withCol[2]) }; + // syntax-error header: "playground-imodel.js:LINE" + const lineOnly = new RegExp(`${fn}:(\\d+)(?!\\d)`).exec(stack); + if (lineOnly) return { line: Number(lineOnly[1]), column: null }; + return {}; +} + +function errPayload(err, phase) { + const stack = err && err.stack ? String(err.stack) : ''; + const loc = locateInStack(stack); + return { + ok: false, + phase, // 'compile' | 'run' | 'resolve' | 'measure' | 'export' + error: { + name: (err && err.name) || 'Error', + message: (err && err.message) || String(err), + line: loc.line ?? null, + column: loc.column ?? null, + stack: stack.split('\n').slice(0, 8).join('\n'), + }, + }; +} + +function summarizeModel(model) { + let pathCount = 0; + let modelCount = 0; + makerjs.model.walk(model, { + onPath: () => { pathCount++; }, + beforeChildWalk: () => { modelCount++; return true; }, + }); + let chainCount = 0; + try { + const chains = makerjs.model.findChains(model); + chainCount = Array.isArray(chains) ? chains.length : 0; + } catch { /* chain analysis is best-effort */ } + return { pathCount, modelCount, chainCount }; +} + +/** + * Execute playground IModel JavaScript headlessly. + * + * @param {string} code The editor contents. + * @param {object} [opts] + * @param {any[]} [opts.params] Kit parameter values (when the code exports a constructor). + * @param {string[]} [opts.exports] Any of: 'svg','dxf','json','pathdata','openjscad','stl'. Defaults to ['svg']. + * @param {object} [opts.svgOptions] Passed through to makerjs.exporter.toSVG. + * @param {object} [opts.extraModules] Extra module id -> object map for require(). + * @returns {object} structured result + */ +export function renderModel(code, opts = {}) { + const wantExports = (opts.exports && opts.exports.length ? opts.exports : ['svg']).map(s => s.toLowerCase()); + const logs = []; + const mockConsole = { + log: (...a) => logs.push(a.map(fmt).join(' ')), + warn: (...a) => logs.push('WARN: ' + a.map(fmt).join(' ')), + error: (...a) => logs.push('ERROR: ' + a.map(fmt).join(' ')), + info: (...a) => logs.push(a.map(fmt).join(' ')), + }; + const moduleObj = { exports: null }; + const mockDocument = { write: () => {} }; + + // ---- compile ----------------------------------------------------------- + let fn; + try { + fn = vm.compileFunction( + String(code), + ['require', 'module', 'document', 'console', 'alert', 'playgroundRender'], + { filename: USER_FILENAME }, + ); + } catch (err) { + return errPayload(err, 'compile'); + } + + // ---- run ------------------------------------------------------------- + let result; + try { + const instance = Reflect.construct(fn, [ + makeSandboxRequire(opts.extraModules || {}), + moduleObj, + mockDocument, + mockConsole, + () => {}, + () => {}, + ]); + result = moduleObj.exports || instance; + } catch (err) { + return { ...errPayload(err, 'run'), console: logs }; + } + + // ---- resolve model (kit vs IModel) -------------------------------- + let model; + let kind; + let usedParams = null; + let metaParameters = null; + try { + if (typeof result === 'function') { + kind = 'kit'; + metaParameters = result.metaParameters || null; + usedParams = Array.isArray(opts.params) && opts.params.length + ? opts.params + : makerjs.kit.getParameterValues(result); + model = makerjs.kit.construct(result, usedParams); + } else if (makerjs.isModel(result)) { + kind = 'model'; + model = result; + } else { + return { + ok: false, + phase: 'resolve', + error: { + name: 'NotAModel', + message: + 'Code did not produce a Maker.js model. Assign this.paths / this.models / this.notes, ' + + 'or set module.exports to a model object or a constructor function.', + line: null, column: null, stack: '', + }, + console: logs, + resultType: typeof result, + }; + } + } catch (err) { + return { ...errPayload(err, 'resolve'), console: logs }; + } + + // ---- measure ------------------------------------------------------ + let extents = null; + try { + const e = makerjs.measure.modelExtents(model); + if (e) { + extents = { + low: e.low, high: e.high, + width: e.high[0] - e.low[0], + height: e.high[1] - e.low[1], + center: e.center, +// eslint-disable-next-line + }; + } + } catch (err) { + return { ...errPayload(err, 'measure'), console: logs }; + } + + const stats = summarizeModel(model); + const emptyModel = !extents || (stats.pathCount === 0); + + // ---- export ----------------------------------------------------- + const outputs = {}; + try { + for (const fmt of wantExports) { + switch (fmt) { + case 'svg': + outputs.svg = makerjs.exporter.toSVG(model, opts.svgOptions || {}); + break; + case 'dxf': + outputs.dxf = makerjs.exporter.toDXF(model); + break; + case 'json': + outputs.json = makerjs.exporter.toJson(model); + break; + case 'pathdata': + outputs.pathdata = makerjs.exporter.toSVGPathData(model); + break; + case 'openjscad': + outputs.openjscad = makerjs.exporter.toJscadScript(model); + break; + case 'stl': + outputs.stl = makerjs.exporter.toJscadSTL + ? makerjs.exporter.toJscadSTL(makerjs.exporter.toJscadCSG(model)) + : undefined; + break; + default: + // ignore unknown format + break; + } + } + } catch (err) { + return { ...errPayload(err, 'export'), console: logs, kind }; + } + + return { + ok: true, + kind, // 'model' | 'kit' + metaParameters, // kit meta params, if any + usedParams, // params actually used to construct a kit + extents, // { low, high, width, height, center } + stats, // { pathCount, modelCount, chainCount } + emptyModel, // true if it rendered but has no measurable geometry + console: logs, // captured console.* output, as strings + outputs, // { svg, dxf, json, pathdata, openjscad, ... } + }; +} + +function fmt(v) { + if (typeof v === 'string') return v; + if (typeof v === 'number' || typeof v === 'boolean') return String(v); + try { return JSON.stringify(v); } catch { return String(v); } +} + +// List built-in makerjs.models.* with their metaParameters (agent API reference). +export function listModels() { + const out = []; + for (const name of Object.keys(makerjs.models).sort()) { + const ctor = makerjs.models[name]; + out.push({ + name, + metaParameters: ctor.metaParameters || null, + defaultParams: (() => { try { return makerjs.kit.getParameterValues(ctor); } catch { return null; } })(), + }); + } + return out; +} + +// Shallow signature index of the makerjs module (namespaces + members). +export function makerjsApi() { + const NS = ['angle', 'point', 'path', 'paths', 'model', 'measure', 'exporter', + 'importer', 'solvers', 'chain', 'kit', 'layout', 'units']; + const api = { version: makerjs.version, namespaces: {} }; + for (const ns of NS) { + const obj = makerjs[ns]; + if (!obj) continue; + api.namespaces[ns] = Object.keys(obj) + .filter(k => typeof obj[k] === 'function' || typeof obj[k] === 'object') + .map(k => { + const member = obj[k]; + if (typeof member === 'function') { + const arity = member.length; + return { name: k, kind: 'function', arity }; + } + return { name: k, kind: 'object' }; + }); + } + api.models = Object.keys(makerjs.models).sort(); + return api; +} diff --git a/tools/playground-mcp/lib/state.mjs b/tools/playground-mcp/lib/state.mjs new file mode 100644 index 000000000..ea108aaad --- /dev/null +++ b/tools/playground-mcp/lib/state.mjs @@ -0,0 +1,74 @@ +// Reads/writes the shared playground code file and browses bundled demo sources. + +import fs from 'node:fs'; +import fsp from 'node:fs/promises'; +import path from 'node:path'; +import { REPO_ROOT, MCP_CODE_FILE } from './playgroundServer.mjs'; + +const DEMOS_DIR = path.join(REPO_ROOT, 'docs', 'demos', 'js'); + +export const STARTER_CODE = `var makerjs = require('makerjs'); + +// Maker.js playground IModel. Assign this.paths / this.models / this.notes, +// or set module.exports to a constructor function with .metaParameters. +this.paths = { + head: new makerjs.paths.Circle([0, 0], 90), + eye: new makerjs.paths.Circle([25, 25], 10), + mouth: new makerjs.paths.Arc([0, 0], 50, 225, 315), + wink: new makerjs.paths.Line([-35, 20], [-15, 20]) +}; +this.notes = '# Maker.js playground (MCP)\\nEdited by an AI agent via the makerjs-playground MCP server.'; +`; + +export function ensureCodeFile() { + fs.mkdirSync(path.dirname(MCP_CODE_FILE), { recursive: true }); + if (!fs.existsSync(MCP_CODE_FILE)) { + fs.writeFileSync(MCP_CODE_FILE, STARTER_CODE, 'utf8'); + } +} + +export async function readCode() { + try { + return await fsp.readFile(MCP_CODE_FILE, 'utf8'); + } catch { + return null; + } +} + +export async function writeCode(code) { + await fsp.mkdir(path.dirname(MCP_CODE_FILE), { recursive: true }); + await fsp.writeFile(MCP_CODE_FILE, String(code), 'utf8'); + return { file: MCP_CODE_FILE, bytes: Buffer.byteLength(String(code), 'utf8') }; +} + +export async function listExamples() { + let names; + try { + names = (await fsp.readdir(DEMOS_DIR)).filter(f => f.endsWith('.js')); + } catch { + return []; + } + const out = []; + for (const f of names.sort()) { + let firstMeaningfulLine = ''; + try { + const src = await fsp.readFile(path.join(DEMOS_DIR, f), 'utf8'); + firstMeaningfulLine = + src.split('\n').map(l => l.trim()) + .find(l => l && !l.startsWith('//') && l !== "var makerjs = require('makerjs');") || ''; + } catch { /* ignore */ } + out.push({ name: f.replace(/\.js$/, ''), file: `docs/demos/js/${f}`, peek: firstMeaningfulLine.slice(0, 120) }); + } + return out; +} + +export async function getExample(name) { + const clean = String(name).replace(/[^a-zA-Z0-9_-]/g, ''); + const file = path.join(DEMOS_DIR, `${clean}.js`); + try { + const src = await fsp.readFile(file, 'utf8'); + return { name: clean, file: `docs/demos/js/${clean}.js`, source: src }; + } catch { + return { name: clean, error: `No demo named "${clean}". Use list_examples to see options.` }; + } +} diff --git a/tools/playground-mcp/package-lock.json b/tools/playground-mcp/package-lock.json new file mode 100644 index 000000000..c94856537 --- /dev/null +++ b/tools/playground-mcp/package-lock.json @@ -0,0 +1,1582 @@ +{ + "name": "makerjs-playground-mcp", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "makerjs-playground-mcp", + "version": "1.0.0", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.30.0", + "http-server": "^14.1.1", + "makerjs": "0.19.2", + "zod": "^3.23.8 || ^4.0.0" + }, + "bin": { + "makerjs-playground-mcp": "server.mjs" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@danmarshall/jscad-typings": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@danmarshall/jscad-typings/-/jscad-typings-1.0.0.tgz", + "integrity": "sha512-MGGIGDItK2UQSsz7yTrXErQXDAFXR3UPxyQ7WZ5RHOwnv60CBXjmkJlXYMYPkSvo+7fUuQL2/ODcvECtc/fi9g==", + "license": "MIT" + }, + "node_modules/@hono/node-server": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-2.1.1.tgz", + "integrity": "sha512-ELuehkj5VCBdgEw9zs+ivkKwyzzUCSQuE96YmiPvn1ECBoZCczbFXJLeEGMTYjphP6gydh4pHMqEYPVMYUVgQg==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.30.0.tgz", + "integrity": "sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA==", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9 || ^2.0.5", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/@types/bezier-js": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/bezier-js/-/bezier-js-0.0.6.tgz", + "integrity": "sha512-kXsAlt8e8N6zt9R6LcMYWB1HkBw3q2g+M9BdI/UE+s4agIONuIscQaRCoInH22+Jas3rw8yLUehL2InaZjyNSA==", + "license": "MIT" + }, + "node_modules/@types/fontkit": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/@types/fontkit/-/fontkit-2.0.9.tgz", + "integrity": "sha512-qNYerFky3muCmZPq+R+B3cUDRA5OONw/oh6aGGFxx2LOBz6yu8eamKusrhkHnC6rc2fm76+G9z9QoWSB2SaQaw==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "7.10.14", + "resolved": "https://registry.npmjs.org/@types/node/-/node-7.10.14.tgz", + "integrity": "sha512-29GS75BE8asnTno3yB6ubOJOO0FboExEqNJy4bpz0GSmW/8wPTNL4h9h63c6s1uTrOopCmJYe/4yJLh5r92ZUA==", + "license": "MIT" + }, + "node_modules/@types/opentype.js": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@types/opentype.js/-/opentype.js-0.7.2.tgz", + "integrity": "sha512-Riz6WyBUBEFs7YqSsJya3SbDHJZ6BmMkY7bzNoue6rtwj+RNilLc+mgOX/eJ0Y0asq16FSU6DatBeOg8ZMy2UQ==", + "license": "MIT" + }, + "node_modules/@types/pdfkit": { + "version": "0.7.36", + "resolved": "https://registry.npmjs.org/@types/pdfkit/-/pdfkit-0.7.36.tgz", + "integrity": "sha512-9eRA6MuW+n78yU3HhoIrDxjyAX2++B5MpLDYqHOnaRTquCw+5sYXT+QN8E1eSaxvNUwlRfU3tOm4UzTeGWmBqg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/bezier-js": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/bezier-js/-/bezier-js-2.6.1.tgz", + "integrity": "sha512-jelZM33eNzcZ9snJ/5HqJLw3IzXvA8RFcBjkdOB8SDYyOvW8Y2tTosojAiBTnD1MhbHoWUYNbxUXxBl61TxbRg==", + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^2.0.0", + "debug": "^4.4.3", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", + "on-finished": "^2.4.1", + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/corser": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/corser/-/corser-2.0.1.tgz", + "integrity": "sha512-utCYNzRSQIZNPIcGZdQc92UVJYAhtGAteCFg0yRaFm8f0P+CPtyGyHXJcGXnffjCybUCEx3FQ2G7U3/o9eIkVQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "license": "MIT" + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.1.tgz", + "integrity": "sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "8.6.2", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.6.2.tgz", + "integrity": "sha512-YH4ru+eOJxQABscKFfRCy9R7x9QFGdezclVMwwgFFndzS2Xnm0uo6B0ABZsLhcpeptGv2qvuJVWlQr9gQZoC3A==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "ip-address": "^10.2.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.6.tgz", + "integrity": "sha512-7Ical1vFEMr0onbVzEDIreM22I4khW+fzyQPwvAFWBp1iwdshSZRsL4jjRvPG9JP1uiqMHRto+YU6R2/CzDz5Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graham_scan": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/graham_scan/-/graham_scan-1.0.5.tgz", + "integrity": "sha512-471KIBS1jOrAHpEStAbOjYI5U7MGBSyqy+wIEnvRYaFDNmpUUhbDZWLRsCSh/OnlY0dCg0NaDyN/2XLoypgIfA==", + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/hono": { + "version": "4.13.5", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.13.5.tgz", + "integrity": "sha512-O6+/eCYRkzzzy0rPWwKLiGBR1nFuUPZynnwjxN1MBA62NNqbT0wQEzQyK2gSO5yDIDB336sXQleAhOHrzlYyKw==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", + "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "license": "MIT", + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/http-server": { + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/http-server/-/http-server-14.1.1.tgz", + "integrity": "sha512-+cbxadF40UXd9T01zUHgA+rlo2Bg1Srer4+B4NwIHdaGxAGGv59nYRnGGDJ9LBk7alpS0US+J+bLLdQOOkJq4A==", + "license": "MIT", + "dependencies": { + "basic-auth": "^2.0.1", + "chalk": "^4.1.2", + "corser": "^2.0.1", + "he": "^1.2.0", + "html-encoding-sniffer": "^3.0.0", + "http-proxy": "^1.18.1", + "mime": "^1.6.0", + "minimist": "^1.2.6", + "opener": "^1.5.1", + "portfinder": "^1.0.28", + "secure-compare": "3.0.1", + "union": "~0.5.0", + "url-join": "^4.0.1" + }, + "bin": { + "http-server": "bin/http-server" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.5.0.tgz", + "integrity": "sha512-R5SnVLJmgYYvf2F2ZgwSBnelz5G4q5AxIC277GDfUaNbrZKNANcBC7RHqYYePlszf4kBolVkJauG0ZjHHFh55g==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jose": { + "version": "6.2.10", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.10.tgz", + "integrity": "sha512-iiW7J9qRFlGxvCOIBDBDxFePQSn7ZMAnrYGhrrOo6siO/MIqwfyilLR27pkfDgUk+raLuzADS8A3S/KLBisc0g==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause" + }, + "node_modules/kdbush": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-2.0.1.tgz", + "integrity": "sha512-9KqSdmWCkBIisFIGclT0FRagKhI7IVbMyUjsxCFG0Ly1Dg6whlxJ7b9lrq8ifk3X/fGeJzok1R75LQfZTfA5zQ==", + "license": "ISC" + }, + "node_modules/makerjs": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/makerjs/-/makerjs-0.19.2.tgz", + "integrity": "sha512-+W30jtxgM5ht8jWyxOpoLpz/6u9DEDjW9XXbvkyAba9xIDXnUB4vaB+JajMm7i6QbD7E41rxSl6VFkOcNMwFBA==", + "license": "Apache-2.0", + "dependencies": { + "@danmarshall/jscad-typings": "^1.0.0", + "@types/bezier-js": "^0.0.6", + "@types/fontkit": "^2.0.8", + "@types/node": "^7.0.5", + "@types/opentype.js": "^0.7.0", + "@types/pdfkit": "^0.7.34", + "bezier-js": "^2.1.0", + "clone": "^1.0.2", + "graham_scan": "^1.0.4", + "kdbush": "^2.0.1" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.1.tgz", + "integrity": "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.1.0.tgz", + "integrity": "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg==", + "license": "MIT", + "dependencies": { + "content-type": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/negotiator/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/opener": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", + "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", + "license": "(WTFPL OR MIT)", + "bin": { + "opener": "bin/opener-bin.js" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/portfinder": { + "version": "1.0.38", + "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.38.tgz", + "integrity": "sha512-rEwq/ZHlJIKw++XtLAO8PPuOQA/zaPJOZJ37BVuN97nLpMJeuDVLVGRwbFoBgLudgdTMP2hdRJP++H+8QOA3vg==", + "license": "MIT", + "dependencies": { + "async": "^3.2.6", + "debug": "^4.3.6" + }, + "engines": { + "node": ">= 10.12" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", + "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "license": "MIT" + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/secure-compare": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/secure-compare/-/secure-compare-3.0.1.tgz", + "integrity": "sha512-AckIIV90rPDcBcglUwXPF3kg0P0qmPsPXAj6BBEENQE1p5yA1xfmDJzfi1Tappj37Pv2mVbKpL3Z1T+Nn7k1Qw==", + "license": "MIT" + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "license": "MIT", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/union": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/union/-/union-0.5.0.tgz", + "integrity": "sha512-N6uOhuW6zO95P3Mel2I2zMsbsanvvtgn6jVqJv4vbVcz/JN0OkL9suomjQGmWtxJQXOCqUJvquc1sMeNz/IwlA==", + "dependencies": { + "qs": "^6.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/url-join": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", + "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==", + "license": "MIT" + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/whatwg-encoding": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", + "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } + } + } +} diff --git a/tools/playground-mcp/package.json b/tools/playground-mcp/package.json new file mode 100644 index 000000000..ac3da192d --- /dev/null +++ b/tools/playground-mcp/package.json @@ -0,0 +1,24 @@ +{ + "name": "makerjs-playground-mcp", + "version": "1.0.0", + "private": true, + "type": "module", + "description": "MCP server that lets an AI agent self-develop and self-debug Maker.js IModel JavaScript for the Maker.js playground", + "main": "server.mjs", + "bin": { + "makerjs-playground-mcp": "server.mjs" + }, + "scripts": { + "start": "node server.mjs", + "selftest": "node test/selftest.mjs" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.30.0", + "http-server": "^14.1.1", + "makerjs": "0.19.2", + "zod": "^3.23.8 || ^4.0.0" + }, + "engines": { + "node": ">=18" + } +} diff --git a/tools/playground-mcp/server.mjs b/tools/playground-mcp/server.mjs new file mode 100644 index 000000000..f5edc7851 --- /dev/null +++ b/tools/playground-mcp/server.mjs @@ -0,0 +1,206 @@ +#!/usr/bin/env node +// makerjs-playground MCP server +// ---------------------------------------------------------------------------- +// Gives an AI agent a fast edit -> render -> inspect -> fix loop for Maker.js +// "IModel" JavaScript (the code in the Maker.js playground's right-hand editor). +// +// Transport: stdio. Start manually with `node server.mjs`, or let Claude Code +// launch it from .mcp.json. + +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; +import { z } from 'zod'; + +import { renderModel, listModels, makerjsApi, makerjs } from './lib/render.mjs'; +import { + startPlayground, stopPlayground, playgroundStatus, playgroundUrl, + MCP_SCRIPT_ID, MCP_CODE_FILE, +} from './lib/playgroundServer.mjs'; +import { + ensureCodeFile, readCode, writeCode, listExamples, getExample, +} from './lib/state.mjs'; + +ensureCodeFile(); + +const server = new McpServer( + { name: 'makerjs-playground', version: '1.0.0' }, + { + instructions: + 'Self-develop and self-debug Maker.js playground IModel JavaScript.\n' + + 'Typical loop: write code -> render_model (headless, returns extents/stats/SVG or a ' + + 'structured error with line/column) -> fix -> repeat. When it looks right, call ' + + 'set_playground_code to push it into the live browser playground ' + + '(playground_start first; open the returned mcpUrl and reload to see changes).\n' + + 'Code shape: assign this.paths / this.models / this.notes, OR set module.exports to a ' + + 'constructor function with a .metaParameters array. require() only resolves "makerjs".', + }, +); + +const json = (obj) => ({ content: [{ type: 'text', text: JSON.stringify(obj, null, 2) }] }); +const text = (s) => ({ content: [{ type: 'text', text: s }] }); + +// --- render_model ----------------------------------------------------------- +server.registerTool( + 'render_model', + { + title: 'Render Maker.js IModel code (headless)', + description: + 'Execute playground IModel JavaScript in a headless Maker.js sandbox and return ' + + 'bounding-box extents, path/model/chain counts, captured console output, and the ' + + 'requested exports (SVG by default). On failure returns {ok:false, phase, error:{name,message,line,column,stack}}. ' + + 'This does NOT touch the browser playground - use it as the fast inner dev loop.', + inputSchema: { + code: z.string().describe('The IModel JavaScript (playground editor contents).'), + params: z.array(z.any()).optional() + .describe('Kit parameter values, in metaParameters order, when the code exports a constructor.'), + exports: z.array(z.enum(['svg', 'dxf', 'json', 'pathdata', 'openjscad', 'stl'])).optional() + .describe('Which export formats to return. Default ["svg"].'), + svgOptions: z.record(z.any()).optional().describe('Options forwarded to makerjs.exporter.toSVG.'), + }, + }, + async ({ code, params, exports, svgOptions }) => { + const result = renderModel(code, { params, exports, svgOptions }); + return json(result); + }, +); + +// --- set_playground_code -------------------------------------------------- +server.registerTool( + 'set_playground_code', + { + title: 'Push code into the live browser playground', + description: + `Validate code with a headless render (unless force=true), then write it to ${'`docs/playground/mcp/current.js`'} ` + + 'so the browser playground shows it at the returned mcpUrl (reload the page to pick up changes). ' + + 'Refuses to write broken code unless force=true.', + inputSchema: { + code: z.string().describe('The IModel JavaScript to publish to the playground.'), + params: z.array(z.any()).optional().describe('Kit parameter values used for the validation render.'), + force: z.boolean().optional().describe('Write even if the validation render fails. Default false.'), + }, + }, + async ({ code, params, force }) => { + const render = renderModel(code, { params, exports: ['svg'] }); + if (!render.ok && !force) { + return json({ written: false, reason: 'validation render failed; pass force:true to write anyway', render }); + } + const w = await writeCode(code); + const st = await playgroundStatus(); + return json({ + written: true, + file: w.file, + bytes: w.bytes, + scriptId: MCP_SCRIPT_ID, + mcpUrl: playgroundUrl(st.port, MCP_SCRIPT_ID), + playgroundRunning: st.running, + hint: st.running + ? 'Open mcpUrl (or reload it) in the browser to see the change.' + : 'Call playground_start, then open mcpUrl.', + render: render.ok + ? { ok: true, kind: render.kind, extents: render.extents, stats: render.stats, emptyModel: render.emptyModel } + : render, + }); + }, +); + +// --- get_playground_code ------------------------------------------------ +server.registerTool( + 'get_playground_code', + { + title: 'Read the current playground code', + description: `Return the contents of ${'`docs/playground/mcp/current.js`'} (what set_playground_code last wrote).`, + inputSchema: {}, + }, + async () => { + const code = await readCode(); + return json({ file: MCP_CODE_FILE, code }); + }, +); + +// --- playground_start / stop / status --------------------------------- +server.registerTool( + 'playground_start', + { + title: 'Start the local playground web server', + description: + 'Start a static http-server for the Maker.js repo (caching disabled) so the playground is ' + + 'reachable in a browser. Idempotent - returns the existing server if one is already up.', + inputSchema: { + port: z.number().int().positive().optional().describe('Port. Default 8020 (or $PLAYGROUND_PORT).'), + }, + }, + async ({ port }) => json(await startPlayground({ port })), +); + +server.registerTool( + 'playground_stop', + { + title: 'Stop the local playground web server', + description: 'Stop the http-server child process started by playground_start.', + inputSchema: {}, + }, + async () => json(stopPlayground()), +); + +server.registerTool( + 'playground_status', + { + title: 'Playground web server status', + description: 'Report whether the playground web server is running, its port, URL, and the mcp script URL.', + inputSchema: {}, + }, + async () => json(await playgroundStatus()), +); + +// --- list_models -------------------------------------------------------- +server.registerTool( + 'list_models', + { + title: 'List built-in Maker.js models', + description: + 'List every constructor under makerjs.models.* with its metaParameters and default parameter values - ' + + 'the building blocks available inside IModel code.', + inputSchema: {}, + }, + async () => json({ version: makerjs.version, count: listModels().length, models: listModels() }), +); + +// --- list_examples / get_example ------------------------------------- +server.registerTool( + 'list_examples', + { + title: 'List bundled playground demo scripts', + description: 'List the demo IModel scripts in docs/demos/js/ (name + one-line peek) to use as reference or starting points.', + inputSchema: {}, + }, + async () => json(await listExamples()), +); + +server.registerTool( + 'get_example', + { + title: 'Get a bundled demo script source', + description: 'Return the full source of a demo from docs/demos/js/ by name (without .js).', + inputSchema: { name: z.string().describe('Demo name, e.g. "dogbone-polygon".') }, + }, + async ({ name }) => json(await getExample(name)), +); + +// --- makerjs_api ------------------------------------------------------- +server.registerTool( + 'makerjs_api', + { + title: 'Maker.js API index', + description: + 'Return a shallow index of the makerjs module: namespaces (paths, model, chain, measure, exporter, layout, ...) ' + + 'with member names and function arity, plus the list of built-in models.', + inputSchema: {}, + }, + async () => json(makerjsApi()), +); + +// --- boot ------------------------------------------------------------- +const transport = new StdioServerTransport(); +await server.connect(transport); +// stderr is safe for logging on stdio transport +console.error(`[makerjs-playground] MCP server ready. code file: ${MCP_CODE_FILE}`); diff --git a/tools/playground-mcp/test/selftest.mjs b/tools/playground-mcp/test/selftest.mjs new file mode 100644 index 000000000..55b73350c --- /dev/null +++ b/tools/playground-mcp/test/selftest.mjs @@ -0,0 +1,188 @@ +// Automated verification for the makerjs-playground MCP server. +// +// node test/selftest.mjs +// +// Exercises: +// 1. the headless render engine (valid model, kit + params, syntax error, +// runtime error, not-a-model, multi-format export) +// 2. the shared code file (write / read round-trip) +// 3. the real MCP server over stdio (initialize, tools/list, tools/call) +// 4. the live playground web server (http reachability of the page and of +// the mcp/current.js script) +// +// Exit code 0 = all pass, 1 = one or more failed. + +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; + +import { renderModel } from '../lib/render.mjs'; +import { writeCode, readCode } from '../lib/state.mjs'; +import { + startPlayground, stopPlayground, playgroundUrl, MCP_SCRIPT_ID, MCP_CODE_URL_PATH, +} from '../lib/playgroundServer.mjs'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const SERVER = path.join(__dirname, '..', 'server.mjs'); + +let pass = 0, fail = 0; +const results = []; +function check(name, cond, detail = '') { + if (cond) { pass++; results.push({ name, status: 'PASS', detail }); console.log(` PASS ${name}${detail ? ' — ' + detail : ''}`); } + else { fail++; results.push({ name, status: 'FAIL', detail }); console.log(` FAIL ${name}${detail ? ' — ' + detail : ''}`); } +} + +// --------------------------------------------------------------------------- +console.log('\n[1] Headless render engine'); + +const smiley = `var makerjs = require('makerjs'); +this.paths = { + head: new makerjs.paths.Circle([0,0], 90), + eye: new makerjs.paths.Circle([25,25], 10), + mouth:new makerjs.paths.Arc([0,0], 50, 225, 315) +}; +this.notes = '# hi';`; +{ + const r = renderModel(smiley, { exports: ['svg', 'dxf', 'json'] }); + check('valid this.paths renders', r.ok && r.kind === 'model', `kind=${r.kind}`); + check('extents are correct', r.ok && r.extents.width === 180 && r.extents.height === 180, + r.ok ? `${r.extents.width}x${r.extents.height}` : 'n/a'); + check('path count = 3', r.ok && r.stats.pathCount === 3, r.ok ? `pathCount=${r.stats.pathCount}` : 'n/a'); + check('SVG export present', r.ok && /^]/.test(r.outputs.svg.trim())); + check('DXF export present', r.ok && typeof r.outputs.dxf === 'string' && r.outputs.dxf.includes('SECTION')); + check('JSON export present', r.ok && typeof r.outputs.json === 'string'); +} + +const kit = `var makerjs = require('makerjs'); +function widget(sides, radius, bone) { + var poly = new makerjs.models.Polygon(sides, radius); + var chain = makerjs.model.findSingleChain(poly); + this.models = { poly: poly, bones: makerjs.chain.dogbone(chain, bone) }; +} +widget.metaParameters = [ + { title:'sides', type:'range', min:3, max:12, value:6 }, + { title:'radius', type:'range', min:10, max:100, value:50 }, + { title:'bone', type:'range', min:0, max:10, value:5 } +]; +module.exports = widget;`; +{ + const r = renderModel(kit, {}); + check('kit detected + constructed with defaults', r.ok && r.kind === 'kit', `usedParams=${JSON.stringify(r.usedParams)}`); + check('kit metaParameters surfaced', r.ok && Array.isArray(r.metaParameters) && r.metaParameters.length === 3); + const r2 = renderModel(kit, { params: [8, 80, 3] }); + check('kit honors explicit params', r2.ok && Math.round(r2.extents.width) === 160, + r2.ok ? `width=${r2.extents.width.toFixed(1)}` : 'n/a'); +} + +{ + const r = renderModel('this.paths = { c: new makerjs.paths.Circle([0,0], 10 };'); + check('syntax error -> ok:false, phase compile', !r.ok && r.phase === 'compile', `name=${r.error.name}`); + check('syntax error reports a line', !r.ok && r.error.line === 1, `line=${r.error.line}`); +} +{ + const r = renderModel(`var makerjs = require('makerjs');\nthis.paths = {};\nnope.doThing();`); + check('runtime error -> ok:false, phase run', !r.ok && r.phase === 'run', `name=${r.error.name}`); + check('runtime error reports line+column', !r.ok && r.error.line === 3 && typeof r.error.column === 'number', + `${r.error.line}:${r.error.column}`); +} +{ + const r = renderModel('var x = 41 + 1;'); + check('not-a-model -> ok:false, phase resolve', !r.ok && r.phase === 'resolve', `name=${r.error.name}`); +} +{ + const r = renderModel(`require('left-pad');`); + check('require() of non-makerjs module is blocked', !r.ok, `${r.error?.name}: ${r.error?.message?.slice(0, 60)}`); +} + +// --------------------------------------------------------------------------- +console.log('\n[2] Shared code file round-trip'); +{ + const marker = `// selftest ${Date.now()}\nvar makerjs = require('makerjs');\nthis.paths = { c: new makerjs.paths.Circle([0,0], 5) };\n`; + const w = await writeCode(marker); + const back = await readCode(); + check('writeCode then readCode returns same bytes', back === marker, `${w.bytes} bytes`); +} + +// --------------------------------------------------------------------------- +console.log('\n[3] MCP server over stdio'); +{ + const transport = new StdioClientTransport({ command: process.execPath, args: [SERVER] }); + const client = new Client({ name: 'selftest', version: '1.0.0' }); + await client.connect(transport); + + const tools = (await client.listTools()).tools; + const names = tools.map(t => t.name).sort(); + const expected = ['get_example', 'get_playground_code', 'list_examples', 'list_models', + 'makerjs_api', 'playground_start', 'playground_status', 'playground_stop', + 'render_model', 'set_playground_code']; + check('tools/list returns all 10 tools', expected.every(n => names.includes(n)), names.join(',')); + + const rc = await client.callTool({ name: 'render_model', arguments: { code: smiley } }); + const rcObj = JSON.parse(rc.content[0].text); + check('tools/call render_model works', rcObj.ok === true && rcObj.stats.pathCount === 3); + + const lm = await client.callTool({ name: 'list_models', arguments: {} }); + const lmObj = JSON.parse(lm.content[0].text); + check('tools/call list_models returns builtins', lmObj.count >= 20, `count=${lmObj.count}`); + + const api = await client.callTool({ name: 'makerjs_api', arguments: {} }); + const apiObj = JSON.parse(api.content[0].text); + check('tools/call makerjs_api returns namespaces', !!apiObj.namespaces.exporter && !!apiObj.namespaces.model); + + const badWrite = await client.callTool({ + name: 'set_playground_code', + arguments: { code: 'this.paths = { c: new makerjs.paths.Circle([0,0] };' }, + }); + const bwObj = JSON.parse(badWrite.content[0].text); + check('set_playground_code refuses broken code', bwObj.written === false); + + const goodWrite = await client.callTool({ + name: 'set_playground_code', + arguments: { code: smiley }, + }); + const gwObj = JSON.parse(goodWrite.content[0].text); + check('set_playground_code writes valid code', gwObj.written === true && gwObj.mcpUrl.includes(MCP_SCRIPT_ID)); + + const getCode = await client.callTool({ name: 'get_playground_code', arguments: {} }); + const gcObj = JSON.parse(getCode.content[0].text); + check('get_playground_code reads it back', gcObj.code === smiley); + + await client.close(); +} + +// --------------------------------------------------------------------------- +console.log('\n[4] Live playground web server'); +let started; +try { + started = await startPlayground({}); + check('playground_start reports a URL', !!started.url, started.url || JSON.stringify(started)); + + const page = await fetch(playgroundUrl(started.port)); + const html = await page.text(); + check('GET /docs/playground/ -> 200 + Maker.js Playground', + page.status === 200 && html.includes('Maker.js Playground'), `status=${page.status}`); + + const scriptRes = await fetch(`http://localhost:${started.port}${MCP_CODE_URL_PATH}`); + const scriptTxt = await scriptRes.text(); + check(`GET ${MCP_CODE_URL_PATH} -> 200 + our code`, + scriptRes.status === 200 && scriptTxt.includes("require('makerjs')"), `status=${scriptRes.status}`); + + // the ?script= URL the playground actually uses must resolve to that same file + const viaScriptParam = await fetch( + `http://localhost:${started.port}/docs/playground/../demos/js/${MCP_SCRIPT_ID}.js`); + check('?script=mcp/current resolves to the code file', viaScriptParam.status === 200, + `status=${viaScriptParam.status}`); +} finally { + const stopped = stopPlayground(); + check('playground_stop stops the managed server', stopped.stopped === true || stopped.reason?.includes('external'), + JSON.stringify(stopped)); +} + +// --------------------------------------------------------------------------- +console.log(`\n──────────────────────────────────────────`); +console.log(`RESULT: ${pass} passed, ${fail} failed`); +console.log(`──────────────────────────────────────────\n`); +// Let stdio pipes / child handles finish closing before exiting (Windows libuv). +process.exitCode = fail === 0 ? 0 : 1; +setTimeout(() => process.exit(process.exitCode), 1500).unref();