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 000000000..4403593c2 Binary files /dev/null and b/tools/playground-mcp/docs/verify-playground.jpg differ 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();