nodality 1.1.8 → 1.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (189) hide show
  1. package/bin/mcp-server.mjs +315 -0
  2. package/bin/nodality.js +16 -0
  3. package/dist/animator.cjs.js.LICENSE.txt +1 -1
  4. package/dist/animator.esm.js.LICENSE.txt +1 -1
  5. package/dist/audionew.cjs.js.LICENSE.txt +1 -1
  6. package/dist/audionew.esm.js.LICENSE.txt +1 -1
  7. package/dist/base.cjs.js.LICENSE.txt +1 -1
  8. package/dist/base.esm.js.LICENSE.txt +1 -1
  9. package/dist/beta-desktop-bar.cjs.js.LICENSE.txt +1 -1
  10. package/dist/beta-desktop-bar.esm.js.LICENSE.txt +1 -1
  11. package/dist/beta-mobile-bar.cjs.js.LICENSE.txt +1 -1
  12. package/dist/beta-mobile-bar.esm.js.LICENSE.txt +1 -1
  13. package/dist/bundle.umd.js.LICENSE.txt +1 -1
  14. package/dist/button.cjs.js.LICENSE.txt +1 -1
  15. package/dist/button.esm.js.LICENSE.txt +1 -1
  16. package/dist/card-getter.cjs.js.LICENSE.txt +1 -1
  17. package/dist/card-getter.esm.js.LICENSE.txt +1 -1
  18. package/dist/center.cjs.js.LICENSE.txt +1 -1
  19. package/dist/center.esm.js.LICENSE.txt +1 -1
  20. package/dist/checkbox.cjs.js.LICENSE.txt +1 -1
  21. package/dist/checkbox.esm.js.LICENSE.txt +1 -1
  22. package/dist/code.cjs.js.LICENSE.txt +1 -1
  23. package/dist/code.esm.js.LICENSE.txt +1 -1
  24. package/dist/container.cjs.js.LICENSE.txt +1 -1
  25. package/dist/container.esm.js.LICENSE.txt +1 -1
  26. package/dist/data-list.cjs.js.LICENSE.txt +1 -1
  27. package/dist/data-list.esm.js.LICENSE.txt +1 -1
  28. package/dist/designer.cjs.js.LICENSE.txt +1 -1
  29. package/dist/designer.esm.js.LICENSE.txt +1 -1
  30. package/dist/element-mapper.cjs.js.LICENSE.txt +1 -1
  31. package/dist/element-mapper.esm.js.LICENSE.txt +1 -1
  32. package/dist/finalresult.esm.js.LICENSE.txt +1 -1
  33. package/dist/flex-card.cjs.js.LICENSE.txt +1 -1
  34. package/dist/flex-card.esm.js.LICENSE.txt +1 -1
  35. package/dist/flex-grid.cjs.js.LICENSE.txt +1 -1
  36. package/dist/flex-grid.esm.js.LICENSE.txt +1 -1
  37. package/dist/flex-row.cjs.js.LICENSE.txt +1 -1
  38. package/dist/flex-row.esm.js.LICENSE.txt +1 -1
  39. package/dist/floating-input.cjs.js.LICENSE.txt +1 -1
  40. package/dist/floating-input.esm.js.LICENSE.txt +1 -1
  41. package/dist/free.cjs.js.LICENSE.txt +1 -1
  42. package/dist/free.esm.js.LICENSE.txt +1 -1
  43. package/dist/horizontal-scroller.cjs.js.LICENSE.txt +1 -1
  44. package/dist/horizontal-scroller.esm.js.LICENSE.txt +1 -1
  45. package/dist/image-picker.cjs.js.LICENSE.txt +1 -1
  46. package/dist/image-picker.esm.js.LICENSE.txt +1 -1
  47. package/dist/image.cjs.js.LICENSE.txt +1 -1
  48. package/dist/image.esm.js.LICENSE.txt +1 -1
  49. package/dist/index.cjs.js.LICENSE.txt +1 -1
  50. package/dist/index.d.ts +1 -1
  51. package/dist/index.esm.js.LICENSE.txt +1 -1
  52. package/dist/keyframe-animation.cjs.js.LICENSE.txt +1 -1
  53. package/dist/keyframe-animation.esm.js.LICENSE.txt +1 -1
  54. package/dist/link-getter.cjs.js.LICENSE.txt +1 -1
  55. package/dist/link-getter.esm.js.LICENSE.txt +1 -1
  56. package/dist/link.cjs.js.LICENSE.txt +1 -1
  57. package/dist/link.esm.js.LICENSE.txt +1 -1
  58. package/dist/meta-adder.cjs.js.LICENSE.txt +1 -1
  59. package/dist/meta-adder.esm.js.LICENSE.txt +1 -1
  60. package/dist/modal-2025.cjs.js.LICENSE.txt +1 -1
  61. package/dist/modal-2025.esm.js.LICENSE.txt +1 -1
  62. package/dist/multiswitcher.cjs.js.LICENSE.txt +1 -1
  63. package/dist/multiswitcher.esm.js.LICENSE.txt +1 -1
  64. package/dist/new-nav-bar.cjs.js.LICENSE.txt +1 -1
  65. package/dist/new-nav-bar.esm.js.LICENSE.txt +1 -1
  66. package/dist/picker.cjs.js.LICENSE.txt +1 -1
  67. package/dist/picker.esm.js.LICENSE.txt +1 -1
  68. package/dist/progress.cjs.js.LICENSE.txt +1 -1
  69. package/dist/progress.esm.js.LICENSE.txt +1 -1
  70. package/dist/radio.cjs.js.LICENSE.txt +1 -1
  71. package/dist/radio.esm.js.LICENSE.txt +1 -1
  72. package/dist/range.cjs.js.LICENSE.txt +1 -1
  73. package/dist/range.esm.js.LICENSE.txt +1 -1
  74. package/dist/scroll-video.cjs.js.LICENSE.txt +1 -1
  75. package/dist/scroll-video.esm.js.LICENSE.txt +1 -1
  76. package/dist/side-bar.cjs.js.LICENSE.txt +1 -1
  77. package/dist/side-bar.esm.js.LICENSE.txt +1 -1
  78. package/dist/side-nav-bar.cjs.js.LICENSE.txt +1 -1
  79. package/dist/side-nav-bar.esm.js.LICENSE.txt +1 -1
  80. package/dist/simple-bar.cjs.js.LICENSE.txt +1 -1
  81. package/dist/simple-bar.esm.js.LICENSE.txt +1 -1
  82. package/dist/slider-2025.cjs.js.LICENSE.txt +1 -1
  83. package/dist/slider-2025.esm.js.LICENSE.txt +1 -1
  84. package/dist/spacer.cjs.js.LICENSE.txt +1 -1
  85. package/dist/spacer.esm.js.LICENSE.txt +1 -1
  86. package/dist/stack.cjs.js.LICENSE.txt +1 -1
  87. package/dist/stack.esm.js.LICENSE.txt +1 -1
  88. package/dist/stacker.cjs.js.LICENSE.txt +1 -1
  89. package/dist/stacker.esm.js.LICENSE.txt +1 -1
  90. package/dist/table.cjs.js.LICENSE.txt +1 -1
  91. package/dist/table.esm.js.LICENSE.txt +1 -1
  92. package/dist/text-field.cjs.js.LICENSE.txt +1 -1
  93. package/dist/text-field.esm.js.LICENSE.txt +1 -1
  94. package/dist/text.cjs.js.LICENSE.txt +1 -1
  95. package/dist/text.esm.js.LICENSE.txt +1 -1
  96. package/dist/theme.cjs.js.LICENSE.txt +1 -1
  97. package/dist/theme.esm.js.LICENSE.txt +1 -1
  98. package/dist/transform-anim.cjs.js.LICENSE.txt +1 -1
  99. package/dist/transform-anim.esm.js.LICENSE.txt +1 -1
  100. package/dist/ulist.cjs.js.LICENSE.txt +1 -1
  101. package/dist/ulist.esm.js.LICENSE.txt +1 -1
  102. package/dist/video.cjs.js.LICENSE.txt +1 -1
  103. package/dist/video.esm.js.LICENSE.txt +1 -1
  104. package/dist/wrap.cjs.js.LICENSE.txt +1 -1
  105. package/dist/wrap.esm.js.LICENSE.txt +1 -1
  106. package/dist/zoom-card.cjs.js.LICENSE.txt +1 -1
  107. package/dist/zoom-card.esm.js.LICENSE.txt +1 -1
  108. package/layout/animator.js +1 -1
  109. package/layout/audio.js +1 -1
  110. package/layout/audionew.js +1 -1
  111. package/layout/base.js +1 -1
  112. package/layout/beta-desktop-bar.js +1 -1
  113. package/layout/beta-mobile-bar.js +1 -1
  114. package/layout/button.js +1 -1
  115. package/layout/center.js +1 -1
  116. package/layout/checkbox.js +1 -1
  117. package/layout/circle.js +1 -1
  118. package/layout/code.js +1 -1
  119. package/layout/container.js +1 -1
  120. package/layout/dropdown-2025.js +1 -1
  121. package/layout/flex-card.js +1 -1
  122. package/layout/flex-grid.js +1 -1
  123. package/layout/flex-row.js +1 -1
  124. package/layout/form-components/custom.js +1 -1
  125. package/layout/form-components/data-list.js +1 -1
  126. package/layout/form-components/floating-input.js +1 -1
  127. package/layout/form-components/form-all.js +1 -1
  128. package/layout/form-components/form.js +1 -1
  129. package/layout/form-components/image-picker.js +1 -1
  130. package/layout/form-components/picker.js +1 -1
  131. package/layout/form-components/radio.js +1 -1
  132. package/layout/form-components/radiogroup.js +1 -1
  133. package/layout/form-components/range.js +1 -1
  134. package/layout/free.js +1 -1
  135. package/layout/grid-switcher.js +1 -1
  136. package/layout/grid.js +1 -1
  137. package/layout/horizontal-scroller.js +1 -1
  138. package/layout/image.js +1 -1
  139. package/layout/index.js +1 -1
  140. package/layout/link.js +1 -1
  141. package/layout/list.js +1 -1
  142. package/layout/meta-adder.js +1 -1
  143. package/layout/modal-2025.js +1 -1
  144. package/layout/morph.js +1 -1
  145. package/layout/multiswitcher.js +1 -1
  146. package/layout/nav-bar.js +1 -1
  147. package/layout/nav-factor/custom-div.js +1 -1
  148. package/layout/new-nav-bar.js +1 -1
  149. package/layout/polygon.js +1 -1
  150. package/layout/prerender-site.js +1 -1
  151. package/layout/prerender.js +1 -1
  152. package/layout/progress.js +1 -1
  153. package/layout/row.js +1 -1
  154. package/layout/scroll-video.js +1 -1
  155. package/layout/side-bar.js +1 -1
  156. package/layout/side-nav-bar.js +1 -1
  157. package/layout/simple-bar.js +1 -1
  158. package/layout/slider-2025.js +1 -1
  159. package/layout/spacer.js +1 -1
  160. package/layout/stack.js +1 -1
  161. package/layout/svg.js +1 -1
  162. package/layout/switcher.js +1 -1
  163. package/layout/table.js +1 -1
  164. package/layout/text-field.js +1 -1
  165. package/layout/text.js +1 -1
  166. package/layout/ulist.js +1 -1
  167. package/layout/video.js +1 -1
  168. package/layout/wrap.js +1 -1
  169. package/layout/zoom-card.js +1 -1
  170. package/lib/card-getter.js +1 -1
  171. package/lib/codegen.js +1 -1
  172. package/lib/data.js +1 -1
  173. package/lib/designer.js +1 -1
  174. package/lib/element-mapper.js +1 -1
  175. package/lib/keyframe-animation.js +1 -1
  176. package/lib/link-getter.js +1 -1
  177. package/lib/morph-node.js +1 -1
  178. package/lib/raster-inspect.js +1 -1
  179. package/lib/raster-ops.js +1 -1
  180. package/lib/raster-presets.js +1 -1
  181. package/lib/scroll-video.js +1 -1
  182. package/lib/seo.js +1 -1
  183. package/lib/stacker.js +1 -1
  184. package/lib/suggest.js +1 -1
  185. package/lib/theme.js +1 -1
  186. package/lib/transform-anim.js +1 -1
  187. package/lib/transition.js +1 -1
  188. package/lib/validate-nodes.js +227 -0
  189. package/package.json +2 -1
@@ -0,0 +1,315 @@
1
+ // mcp-server.mjs — M5, the agent harness.
2
+ //
3
+ // A Model Context Protocol server over stdio exposing the two-array API
4
+ // to agent IDEs. Three tools, and no rendering or vocabulary knowledge of
5
+ // its own: when a tool needed something the library did not have — a
6
+ // validator for a node INSTANCE, as opposed to the op-definition check
7
+ // that already existed — that validator was added to the library rather
8
+ // than to this file. The rule is the plan's: if a tool needs code the
9
+ // core does not have, the core is missing something.
10
+ //
11
+ // WHY THIS IS HAND-ROLLED
12
+ //
13
+ // The MCP SDK is a dependency, and this package takes none. The stdio
14
+ // transport is newline-delimited JSON-RPC 2.0 with a Content-Length
15
+ // framing option; the newline form is small enough to own outright,
16
+ // which is the same reasoning applied to the allocator. What follows is
17
+ // the whole protocol surface this server needs: a handshake, a tool
18
+ // list, and a dispatch.
19
+ //
20
+ // WHY THE ERROR REPORT MATTERS MORE THAN THE HAPPY PATH
21
+ //
22
+ // `validateNodes` returns a machine-readable report — a code, the path
23
+ // that failed, what was found, and did-you-mean suggestions drawn from
24
+ // the live registry. That report is the entire point of the server. An
25
+ // agent that writes `"dithr"` should get back `dither` and repair itself
26
+ // in one turn, rather than receiving nothing at all and concluding the
27
+ // effect does not work — which is what happened before, because an
28
+ // unknown op is silent. So tool-level failures return `isError: true`
29
+ // carrying that same report, never a thrown exception and never prose.
30
+
31
+ // WHAT THIS SERVER IS POINTED AT
32
+ //
33
+ // The shipped two-array API: `E` describes what exists, `N` describes
34
+ // what is done to it, and `N` carries the composable raster chain and
35
+ // the `{ op: "morph" }` transition node — the navbar-becomes-a-card
36
+ // surface. An earlier draft of this server wrapped `layout/morph.js`,
37
+ // which is a different thing that shares the word: a layout-variant spec
38
+ // DSL of grid bones and 0-1 axes. Both ship, but this is the one an
39
+ // agent writing a page actually needs, and the one whose vocabulary is
40
+ // large enough that guessing at it fails.
41
+ import { inspect } from "node:util";
42
+ import { validateNodes, describeOps } from "../lib/validate-nodes.js";
43
+
44
+ const PROTOCOL = "2025-06-18";
45
+ // Versions whose envelope this server is known to speak. A client asking
46
+ // for one of these gets its own version echoed back; anything else gets
47
+ // ours, which is what the specification asks for.
48
+ const KNOWN = new Set(["2024-11-05", "2025-03-26", "2025-06-18"]);
49
+
50
+ const pkgVersion = async () => {
51
+ try {
52
+ const { readFile } = await import("node:fs/promises");
53
+ const url = new URL("../package.json", import.meta.url);
54
+ return JSON.parse(await readFile(url, "utf8")).version || "0.0.0";
55
+ } catch { return "0.0.0"; }
56
+ };
57
+
58
+ // ── the three tools ──────────────────────────────────────────────────
59
+
60
+ const ELEMENTS_SCHEMA = {
61
+ type: "array",
62
+ description:
63
+ "E — what exists. Each item is an element descriptor: `type` picks " +
64
+ "the component, `id` is the handle nodes aim at, `children` nests " +
65
+ "further descriptors, and every other key is forwarded to that " +
66
+ "component. Nothing in E names an effect.",
67
+ items: { type: "object" },
68
+ };
69
+
70
+ const NODES_SCHEMA = {
71
+ type: "array",
72
+ description:
73
+ "N — what happens to it. Three families, told apart by `op`: " +
74
+ "`{ op: \"dither\", target: [\"hero\"], levels: 6 }` is a raster op " +
75
+ "(several aimed at one element compose into a single shader pass, in " +
76
+ "array order); `{ op: { name: \"shadow\" }, target: [...] }` is a " +
77
+ "design node; `{ op: \"morph\", from: \"topnav\", to: { \"About\": " +
78
+ "\"about\" }, effect: \"t-vhs\", duration: 900, back: true }` is a " +
79
+ "transition — one element becomes another on interaction. Nothing in " +
80
+ "N names a component; the two arrays meet only through `target`, " +
81
+ "`from` and `to`. Call list_ops first for the exact vocabulary.",
82
+ items: { type: "object" },
83
+ };
84
+
85
+ const TOOLS = [
86
+ {
87
+ name: "list_ops",
88
+ description:
89
+ "The vocabulary, as data: every raster op with its pipeline stage " +
90
+ "and its parameters (name, default, unit, whether changing it " +
91
+ "rebuilds the shader), plus the parameters every node inherits " +
92
+ "(target, by, side, ease...), the driver names, the easing names " +
93
+ "and the unit names. Read this before writing nodes — it is " +
94
+ "assembled from the live registry, so it cannot drift from what " +
95
+ "the pipeline accepts.",
96
+ inputSchema: { type: "object", properties: {} },
97
+ },
98
+ {
99
+ name: "validate_nodes",
100
+ description:
101
+ "Check an N array before applying it, and return a machine-readable " +
102
+ "report. Never throws: malformed input IS a report. Each error " +
103
+ "carries a code, the path that failed, the offending value, " +
104
+ "did-you-mean suggestions and the valid vocabulary for that " +
105
+ "position — enough to repair without re-reading the docs. Pass " +
106
+ "elements too and targets are checked against real ids, which " +
107
+ "catches the other silent failure: a correctly spelled op aimed at " +
108
+ "an element that does not exist.",
109
+ inputSchema: {
110
+ type: "object",
111
+ properties: { nodes: NODES_SCHEMA, elements: ELEMENTS_SCHEMA },
112
+ required: ["nodes"],
113
+ },
114
+ },
115
+ {
116
+ name: "preview",
117
+ description:
118
+ "Render an (E, N) pair to a self-contained HTML file through the " +
119
+ "build-time prerenderer (jsdom; no browser is launched). Returns " +
120
+ "{ path, bytes, note }. Invalid nodes return the validation report " +
121
+ "instead. Note the boundary: prerendering produces the DOM and the " +
122
+ "morph scaffolding, but raster effects and transitions need a GPU " +
123
+ "and run only when the file is opened in a browser.",
124
+ inputSchema: {
125
+ type: "object",
126
+ properties: {
127
+ elements: ELEMENTS_SCHEMA,
128
+ nodes: NODES_SCHEMA,
129
+ output: {
130
+ type: "string",
131
+ description:
132
+ "Where to write the HTML. Defaults to a file in the system " +
133
+ "temp directory.",
134
+ },
135
+ },
136
+ required: ["elements"],
137
+ },
138
+ },
139
+ ];
140
+
141
+ // ── tool implementations ─────────────────────────────────────────────
142
+
143
+ /** Shape a validation report as a failed tool result. */
144
+ const reportAsError = (report) => ({
145
+ content: [{ type: "text", text: JSON.stringify(report, null, 2) }],
146
+ isError: true,
147
+ });
148
+ const ok = (value) => ({
149
+ content: [{ type: "text", text: JSON.stringify(value, null, 2) }],
150
+ });
151
+
152
+ const IMPL = {
153
+ list_ops: async () => ok(describeOps()),
154
+
155
+ // Verbatim: the report is the product, and reshaping it here would put
156
+ // this server's opinion between the core and the agent.
157
+ validate_nodes: async ({ nodes, elements }) =>
158
+ ok(validateNodes(nodes, elements)),
159
+
160
+ preview: async ({ elements, nodes, output }) => {
161
+ const N = nodes || [];
162
+ const report = validateNodes(N, elements);
163
+ if (!report.ok) return reportAsError(report);
164
+
165
+ const [{ prerender }, { writeFile, mkdtemp, readFile }, { tmpdir }, { join }] =
166
+ await Promise.all([
167
+ import("../layout/prerender.js"),
168
+ import("node:fs/promises"),
169
+ import("node:os"),
170
+ import("node:path"),
171
+ ]);
172
+
173
+ const dir = await mkdtemp(join(tmpdir(), "nodality-preview-"));
174
+ const template = join(dir, "index.html");
175
+ const out = output || join(dir, "preview.html");
176
+
177
+ // The prerenderer wants a template on disk and a build function; it
178
+ // renders into `mount` and writes the result. Going through the
179
+ // documented SSG entry point rather than reaching into jsdom keeps
180
+ // this server free of any rendering knowledge of its own.
181
+ await writeFile(template,
182
+ `<!DOCTYPE html><html><head><meta charset="utf-8">` +
183
+ `<title>Nodality preview</title></head><body>` +
184
+ `<div id="mount"></div></body></html>`, "utf8");
185
+
186
+ await prerender({
187
+ template,
188
+ mount: "#mount",
189
+ output: out,
190
+ // `build` is handed the jsdom window and is expected to render
191
+ // into it. The Designer is imported HERE, inside the callback,
192
+ // rather than at module scope: prerender installs the DOM globals
193
+ // before calling this, and library modules read `document` as they
194
+ // load, so an earlier import would bind to a document that does
195
+ // not exist yet.
196
+ build: async () => {
197
+ const { Des } = await import("../lib/designer.js");
198
+ new Des().nodes(N).add(elements).set({ mount: "#mount", code: false });
199
+ },
200
+ });
201
+
202
+ const bytes = (await readFile(out)).length;
203
+ return ok({
204
+ path: out, bytes,
205
+ // Said plainly, because an agent cannot see the result and would
206
+ // otherwise report a shader that never ran as working.
207
+ note: "Prerendered DOM only. Raster effects and morph transitions " +
208
+ "need a GPU and run when this file is opened in a browser.",
209
+ });
210
+ },
211
+ };
212
+
213
+ // ── JSON-RPC 2.0 over newline-delimited stdio ────────────────────────
214
+
215
+ const write = (msg) => process.stdout.write(JSON.stringify(msg) + "\n");
216
+ const reply = (id, result) => write({ jsonrpc: "2.0", id, result });
217
+ const fail = (id, code, message) =>
218
+ write({ jsonrpc: "2.0", id, error: { code, message } });
219
+
220
+ async function handle(msg) {
221
+ const { id, method, params } = msg;
222
+
223
+ // Notifications carry no id and expect no reply. `initialized` is the
224
+ // one every client sends; answering it is a protocol error.
225
+ if (id === undefined || id === null) return;
226
+
227
+ if (method === "initialize") {
228
+ const asked = params && params.protocolVersion;
229
+ return reply(id, {
230
+ protocolVersion: KNOWN.has(asked) ? asked : PROTOCOL,
231
+ capabilities: { tools: {} },
232
+ serverInfo: { name: "nodality-morph", version: await pkgVersion() },
233
+ });
234
+ }
235
+
236
+ if (method === "tools/list") return reply(id, { tools: TOOLS });
237
+
238
+ if (method === "tools/call") {
239
+ const name = params && params.name;
240
+ const args = (params && params.arguments) || {};
241
+ const impl = IMPL[name];
242
+ // -32602 is "invalid params" — the right code for a tool that does
243
+ // not exist, as distinct from a tool that ran and rejected its input.
244
+ if (!impl) return fail(id, -32602, `Unknown tool: ${name}`);
245
+ try {
246
+ return reply(id, await impl(args));
247
+ } catch (e) {
248
+ // A throw here is a bug in this server or an unreachable filesystem,
249
+ // not bad user input — bad user input became a report above. Report
250
+ // it as a tool error so the agent sees it rather than hanging.
251
+ return reply(id, {
252
+ content: [{ type: "text", text: String((e && e.stack) || e) }],
253
+ isError: true,
254
+ });
255
+ }
256
+ }
257
+
258
+ return fail(id, -32601, `Method not found: ${method}`);
259
+ }
260
+
261
+ /**
262
+ * stdout belongs to the protocol, and to nothing else.
263
+ *
264
+ * This is not a precaution, it is a fix. `preview` renders through the
265
+ * prerenderer, and the component layer prints as it renders — a single
266
+ * `console.log` of an options object lands in the middle of a JSON-RPC
267
+ * frame, the host fails to parse it, and the connection dies with an
268
+ * error that names JSON rather than the component that spoke. Every
269
+ * console method is therefore rebound to stderr, where a host displays
270
+ * it as server log output and where it can do no damage.
271
+ */
272
+ function claimStdout() {
273
+ // `inspect`, not JSON.stringify: what the component layer prints
274
+ // includes DOM nodes and option objects with circular references, and
275
+ // stringify would throw inside the very handler meant to keep the
276
+ // stream clean.
277
+ const fmt = (v) => (typeof v === "string" ? v : inspect(v, { depth: 2 }));
278
+ for (const method of ["log", "info", "warn", "debug", "trace", "dir"]) {
279
+ console[method] = (...args) =>
280
+ process.stderr.write(args.map(fmt).join(" ") + "\n");
281
+ }
282
+ }
283
+
284
+ export function serve(input = process.stdin, output = process.stdout) {
285
+ if (output !== process.stdout) throw new Error("serve: stdout only");
286
+ claimStdout();
287
+ let buffer = "";
288
+ input.setEncoding("utf8");
289
+ input.on("data", async (chunk) => {
290
+ buffer += chunk;
291
+ let nl;
292
+ while ((nl = buffer.indexOf("\n")) >= 0) {
293
+ const line = buffer.slice(0, nl).trim();
294
+ buffer = buffer.slice(nl + 1);
295
+ if (!line) continue;
296
+ let msg;
297
+ try {
298
+ msg = JSON.parse(line);
299
+ } catch {
300
+ write({ jsonrpc: "2.0", id: null,
301
+ error: { code: -32700, message: "Parse error" } });
302
+ continue;
303
+ }
304
+ await handle(msg);
305
+ }
306
+ });
307
+ // A closed stdin means the host is gone; exiting keeps no orphans.
308
+ input.on("end", () => process.exit(0));
309
+ }
310
+
311
+ // Run when invoked directly (`node bin/mcp-server.mjs`), so the server
312
+ // can be driven without the CLI wrapper — which is what the tests do.
313
+ if (process.argv[1] && import.meta.url.endsWith(process.argv[1].split("/").pop())) {
314
+ serve();
315
+ }
package/bin/nodality.js CHANGED
@@ -48,6 +48,13 @@ function showUsage() {
48
48
  # version into upload/_nodality-version.js.
49
49
  # Run from consumer upgrade.sh after
50
50
  # \`npm install nodality@latest\`.
51
+ nodality mcp # Model Context Protocol server over stdio,
52
+ # exposing the morph core to agent IDEs:
53
+ # validate_spec, expand_spec, preview.
54
+ # Not run by hand — registered with a host:
55
+ # { "mcpServers": { "nodality": {
56
+ # "command": "npx",
57
+ # "args": ["nodality", "mcp"] } } }
51
58
  nodality help
52
59
 
53
60
  Compile flags:
@@ -795,6 +802,15 @@ async function main() {
795
802
  await runFanoutStandalone(rest);
796
803
  } else if (command === "stage") {
797
804
  await runStage(rest);
805
+ } else if (command === "mcp") {
806
+ // Lazily imported so that prerender/compile/stage pay nothing for a
807
+ // subcommand they never touch, and so a broken morph core cannot
808
+ // take the rest of the CLI down at import time.
809
+ const { serve } = await import("./mcp-server.mjs");
810
+ serve();
811
+ // serve() returns immediately; the process stays alive on stdin and
812
+ // exits when the host closes it.
813
+ await new Promise(() => {});
798
814
  } else {
799
815
  console.error(`[nodality] Unknown command: ${command}`);
800
816
  showUsage();
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.1.7
2
+ * nodality v1.1.8
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.1.7
2
+ * nodality v1.1.8
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.1.7
2
+ * nodality v1.1.8
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.1.7
2
+ * nodality v1.1.8
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.1.7
2
+ * nodality v1.1.8
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.1.7
2
+ * nodality v1.1.8
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.1.7
2
+ * nodality v1.1.8
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.1.7
2
+ * nodality v1.1.8
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.1.7
2
+ * nodality v1.1.8
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.1.7
2
+ * nodality v1.1.8
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -50,7 +50,7 @@
50
50
  */
51
51
 
52
52
  /*!
53
- * nodality v1.1.7
53
+ * nodality v1.1.8
54
54
  * (c) 2026 Filip Vabrousek
55
55
  * License: MIT
56
56
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.1.7
2
+ * nodality v1.1.8
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.1.7
2
+ * nodality v1.1.8
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.1.7
2
+ * nodality v1.1.8
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.1.7
2
+ * nodality v1.1.8
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.1.7
2
+ * nodality v1.1.8
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.1.7
2
+ * nodality v1.1.8
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.1.7
2
+ * nodality v1.1.8
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.1.7
2
+ * nodality v1.1.8
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.1.7
2
+ * nodality v1.1.8
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.1.7
2
+ * nodality v1.1.8
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.1.7
2
+ * nodality v1.1.8
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.1.7
2
+ * nodality v1.1.8
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.1.7
2
+ * nodality v1.1.8
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.1.7
2
+ * nodality v1.1.8
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -50,7 +50,7 @@
50
50
  */
51
51
 
52
52
  /*!
53
- * nodality v1.1.7
53
+ * nodality v1.1.8
54
54
  * (c) 2026 Filip Vabrousek
55
55
  * License: MIT
56
56
  */
@@ -50,7 +50,7 @@
50
50
  */
51
51
 
52
52
  /*!
53
- * nodality v1.1.7
53
+ * nodality v1.1.8
54
54
  * (c) 2026 Filip Vabrousek
55
55
  * License: MIT
56
56
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.1.7
2
+ * nodality v1.1.8
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.1.7
2
+ * nodality v1.1.8
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -50,7 +50,7 @@
50
50
  */
51
51
 
52
52
  /*!
53
- * nodality v1.1.7
53
+ * nodality v1.1.8
54
54
  * (c) 2026 Filip Vabrousek
55
55
  * License: MIT
56
56
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.1.7
2
+ * nodality v1.1.8
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.1.7
2
+ * nodality v1.1.8
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.1.7
2
+ * nodality v1.1.8
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.1.7
2
+ * nodality v1.1.8
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.1.7
2
+ * nodality v1.1.8
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.1.7
2
+ * nodality v1.1.8
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.1.7
2
+ * nodality v1.1.8
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.1.7
2
+ * nodality v1.1.8
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */