nodality 1.1.7 → 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.
- package/bin/mcp-server.mjs +315 -0
- package/bin/nodality.js +16 -0
- package/dist/animator.cjs.js +1 -1
- package/dist/animator.cjs.js.LICENSE.txt +1 -1
- package/dist/animator.esm.js.LICENSE.txt +1 -1
- package/dist/audionew.cjs.js +1 -1
- package/dist/audionew.cjs.js.LICENSE.txt +1 -1
- package/dist/audionew.esm.js.LICENSE.txt +1 -1
- package/dist/base.cjs.js +1 -1
- package/dist/base.cjs.js.LICENSE.txt +1 -1
- package/dist/base.esm.js.LICENSE.txt +1 -1
- package/dist/beta-desktop-bar.cjs.js +1 -1
- package/dist/beta-desktop-bar.cjs.js.LICENSE.txt +1 -1
- package/dist/beta-desktop-bar.esm.js.LICENSE.txt +1 -1
- package/dist/beta-mobile-bar.cjs.js +1 -1
- package/dist/beta-mobile-bar.cjs.js.LICENSE.txt +1 -1
- package/dist/beta-mobile-bar.esm.js.LICENSE.txt +1 -1
- package/dist/bundle.umd.js +1 -1
- package/dist/bundle.umd.js.LICENSE.txt +1 -1
- package/dist/button.cjs.js +1 -1
- package/dist/button.cjs.js.LICENSE.txt +1 -1
- package/dist/button.esm.js.LICENSE.txt +1 -1
- package/dist/card-getter.cjs.js.LICENSE.txt +1 -1
- package/dist/card-getter.esm.js.LICENSE.txt +1 -1
- package/dist/center.cjs.js +1 -1
- package/dist/center.cjs.js.LICENSE.txt +1 -1
- package/dist/center.esm.js.LICENSE.txt +1 -1
- package/dist/checkbox.cjs.js +1 -1
- package/dist/checkbox.cjs.js.LICENSE.txt +1 -1
- package/dist/checkbox.esm.js.LICENSE.txt +1 -1
- package/dist/code.cjs.js +1 -1
- package/dist/code.cjs.js.LICENSE.txt +1 -1
- package/dist/code.esm.js.LICENSE.txt +1 -1
- package/dist/container.cjs.js +1 -1
- package/dist/container.cjs.js.LICENSE.txt +1 -1
- package/dist/container.esm.js.LICENSE.txt +1 -1
- package/dist/data-list.cjs.js.LICENSE.txt +1 -1
- package/dist/data-list.esm.js.LICENSE.txt +1 -1
- package/dist/designer.cjs.js +1 -1
- package/dist/designer.cjs.js.LICENSE.txt +1 -1
- package/dist/designer.esm.js.LICENSE.txt +1 -1
- package/dist/element-mapper.cjs.js +1 -1
- package/dist/element-mapper.cjs.js.LICENSE.txt +1 -1
- package/dist/element-mapper.esm.js.LICENSE.txt +1 -1
- package/dist/finalresult.esm.js +1 -1
- package/dist/finalresult.esm.js.LICENSE.txt +1 -1
- package/dist/flex-card.cjs.js +1 -1
- package/dist/flex-card.cjs.js.LICENSE.txt +1 -1
- package/dist/flex-card.esm.js.LICENSE.txt +1 -1
- package/dist/flex-grid.cjs.js +1 -1
- package/dist/flex-grid.cjs.js.LICENSE.txt +1 -1
- package/dist/flex-grid.esm.js.LICENSE.txt +1 -1
- package/dist/flex-row.cjs.js +1 -1
- package/dist/flex-row.cjs.js.LICENSE.txt +1 -1
- package/dist/flex-row.esm.js.LICENSE.txt +1 -1
- package/dist/floating-input.cjs.js +1 -1
- package/dist/floating-input.cjs.js.LICENSE.txt +1 -1
- package/dist/floating-input.esm.js.LICENSE.txt +1 -1
- package/dist/free.cjs.js +1 -1
- package/dist/free.cjs.js.LICENSE.txt +1 -1
- package/dist/free.esm.js.LICENSE.txt +1 -1
- package/dist/horizontal-scroller.cjs.js.LICENSE.txt +1 -1
- package/dist/horizontal-scroller.esm.js.LICENSE.txt +1 -1
- package/dist/image-picker.cjs.js +1 -1
- package/dist/image-picker.cjs.js.LICENSE.txt +1 -1
- package/dist/image-picker.esm.js.LICENSE.txt +1 -1
- package/dist/image.cjs.js +1 -1
- package/dist/image.cjs.js.LICENSE.txt +1 -1
- package/dist/image.esm.js.LICENSE.txt +1 -1
- package/dist/index.cjs.js +1 -1
- package/dist/index.cjs.js.LICENSE.txt +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.esm.js.LICENSE.txt +1 -1
- package/dist/keyframe-animation.cjs.js.LICENSE.txt +1 -1
- package/dist/keyframe-animation.esm.js.LICENSE.txt +1 -1
- package/dist/link-getter.cjs.js +1 -1
- package/dist/link-getter.cjs.js.LICENSE.txt +1 -1
- package/dist/link-getter.esm.js.LICENSE.txt +1 -1
- package/dist/link.cjs.js +1 -1
- package/dist/link.cjs.js.LICENSE.txt +1 -1
- package/dist/link.esm.js.LICENSE.txt +1 -1
- package/dist/meta-adder.cjs.js +1 -1
- package/dist/meta-adder.cjs.js.LICENSE.txt +1 -1
- package/dist/meta-adder.esm.js.LICENSE.txt +1 -1
- package/dist/modal-2025.cjs.js +1 -1
- package/dist/modal-2025.cjs.js.LICENSE.txt +1 -1
- package/dist/modal-2025.esm.js.LICENSE.txt +1 -1
- package/dist/multiswitcher.cjs.js +1 -1
- package/dist/multiswitcher.cjs.js.LICENSE.txt +1 -1
- package/dist/multiswitcher.esm.js.LICENSE.txt +1 -1
- package/dist/new-nav-bar.cjs.js +1 -1
- package/dist/new-nav-bar.cjs.js.LICENSE.txt +1 -1
- package/dist/new-nav-bar.esm.js.LICENSE.txt +1 -1
- package/dist/picker.cjs.js +1 -1
- package/dist/picker.cjs.js.LICENSE.txt +1 -1
- package/dist/picker.esm.js.LICENSE.txt +1 -1
- package/dist/progress.cjs.js +1 -1
- package/dist/progress.cjs.js.LICENSE.txt +1 -1
- package/dist/progress.esm.js.LICENSE.txt +1 -1
- package/dist/radio.cjs.js.LICENSE.txt +1 -1
- package/dist/radio.esm.js.LICENSE.txt +1 -1
- package/dist/range.cjs.js.LICENSE.txt +1 -1
- package/dist/range.esm.js.LICENSE.txt +1 -1
- package/dist/scroll-video.cjs.js.LICENSE.txt +1 -1
- package/dist/scroll-video.esm.js.LICENSE.txt +1 -1
- package/dist/side-bar.cjs.js +1 -1
- package/dist/side-bar.cjs.js.LICENSE.txt +1 -1
- package/dist/side-bar.esm.js.LICENSE.txt +1 -1
- package/dist/side-nav-bar.cjs.js +1 -1
- package/dist/side-nav-bar.cjs.js.LICENSE.txt +1 -1
- package/dist/side-nav-bar.esm.js.LICENSE.txt +1 -1
- package/dist/simple-bar.cjs.js +1 -1
- package/dist/simple-bar.cjs.js.LICENSE.txt +1 -1
- package/dist/simple-bar.esm.js.LICENSE.txt +1 -1
- package/dist/slider-2025.cjs.js +1 -1
- package/dist/slider-2025.cjs.js.LICENSE.txt +1 -1
- package/dist/slider-2025.esm.js.LICENSE.txt +1 -1
- package/dist/spacer.cjs.js +1 -1
- package/dist/spacer.cjs.js.LICENSE.txt +1 -1
- package/dist/spacer.esm.js.LICENSE.txt +1 -1
- package/dist/stack.cjs.js +1 -1
- package/dist/stack.cjs.js.LICENSE.txt +1 -1
- package/dist/stack.esm.js.LICENSE.txt +1 -1
- package/dist/stacker.cjs.js.LICENSE.txt +1 -1
- package/dist/stacker.esm.js.LICENSE.txt +1 -1
- package/dist/table.cjs.js +1 -1
- package/dist/table.cjs.js.LICENSE.txt +1 -1
- package/dist/table.esm.js.LICENSE.txt +1 -1
- package/dist/text-field.cjs.js +1 -1
- package/dist/text-field.cjs.js.LICENSE.txt +1 -1
- package/dist/text-field.esm.js.LICENSE.txt +1 -1
- package/dist/text.cjs.js +1 -1
- package/dist/text.cjs.js.LICENSE.txt +1 -1
- package/dist/text.esm.js.LICENSE.txt +1 -1
- package/dist/theme.cjs.js.LICENSE.txt +1 -1
- package/dist/theme.esm.js.LICENSE.txt +1 -1
- package/dist/transform-anim.cjs.js.LICENSE.txt +1 -1
- package/dist/transform-anim.esm.js.LICENSE.txt +1 -1
- package/dist/ulist.cjs.js +1 -1
- package/dist/ulist.cjs.js.LICENSE.txt +1 -1
- package/dist/ulist.esm.js.LICENSE.txt +1 -1
- package/dist/video.cjs.js +1 -1
- package/dist/video.cjs.js.LICENSE.txt +1 -1
- package/dist/video.esm.js.LICENSE.txt +1 -1
- package/dist/wrap.cjs.js +1 -1
- package/dist/wrap.cjs.js.LICENSE.txt +1 -1
- package/dist/wrap.esm.js.LICENSE.txt +1 -1
- package/dist/zoom-card.cjs.js +1 -1
- package/dist/zoom-card.cjs.js.LICENSE.txt +1 -1
- package/dist/zoom-card.esm.js.LICENSE.txt +1 -1
- package/layout/animator.js +1 -1
- package/layout/audio.js +1 -1
- package/layout/audionew.js +1 -1
- package/layout/base.js +1 -1
- package/layout/beta-desktop-bar.js +1 -1
- package/layout/beta-mobile-bar.js +1 -1
- package/layout/button.js +1 -1
- package/layout/center.js +1 -1
- package/layout/checkbox.js +1 -1
- package/layout/circle.js +1 -1
- package/layout/code.js +1 -1
- package/layout/container.js +1 -1
- package/layout/dropdown-2025.js +1 -1
- package/layout/flex-card.js +1 -1
- package/layout/flex-grid.js +1 -1
- package/layout/flex-row.js +1 -1
- package/layout/form-components/custom.js +1 -1
- package/layout/form-components/data-list.js +1 -1
- package/layout/form-components/floating-input.js +1 -1
- package/layout/form-components/form-all.js +1 -1
- package/layout/form-components/form.js +1 -1
- package/layout/form-components/image-picker.js +1 -1
- package/layout/form-components/picker.js +1 -1
- package/layout/form-components/radio.js +1 -1
- package/layout/form-components/radiogroup.js +1 -1
- package/layout/form-components/range.js +1 -1
- package/layout/free.js +1 -1
- package/layout/grid-switcher.js +1 -1
- package/layout/grid.js +1 -1
- package/layout/horizontal-scroller.js +1 -1
- package/layout/image.js +1 -1
- package/layout/index.js +1 -1
- package/layout/link.js +1 -1
- package/layout/list.js +1 -1
- package/layout/meta-adder.js +1 -1
- package/layout/modal-2025.js +1 -1
- package/layout/morph.js +1 -1
- package/layout/multiswitcher.js +1 -1
- package/layout/nav-bar.js +1 -1
- package/layout/nav-factor/custom-div.js +1 -1
- package/layout/new-nav-bar.js +1 -1
- package/layout/polygon.js +1 -1
- package/layout/prerender-site.js +1 -1
- package/layout/prerender.js +1 -1
- package/layout/progress.js +1 -1
- package/layout/row.js +1 -1
- package/layout/scroll-video.js +1 -1
- package/layout/side-bar.js +1 -1
- package/layout/side-nav-bar.js +1 -1
- package/layout/simple-bar.js +1 -1
- package/layout/slider-2025.js +1 -1
- package/layout/spacer.js +1 -1
- package/layout/stack.js +1 -1
- package/layout/svg.js +1 -1
- package/layout/switcher.js +1 -1
- package/layout/table.js +1 -1
- package/layout/text-field.js +1 -1
- package/layout/text.js +1 -1
- package/layout/ulist.js +1 -1
- package/layout/video.js +1 -1
- package/layout/wrap.js +1 -1
- package/layout/zoom-card.js +1 -1
- package/lib/card-getter.js +1 -1
- package/lib/codegen.js +1 -1
- package/lib/data.js +1 -1
- package/lib/designer.js +1 -1
- package/lib/element-mapper.js +1 -1
- package/lib/keyframe-animation.js +1 -1
- package/lib/link-getter.js +1 -1
- package/lib/morph-node.js +1 -1
- package/lib/raster-inspect.js +1 -1
- package/lib/raster-ops.js +20 -2
- package/lib/raster-presets.js +1 -1
- package/lib/scroll-video.js +1 -1
- package/lib/seo.js +1 -1
- package/lib/stacker.js +1 -1
- package/lib/suggest.js +1 -1
- package/lib/theme.js +1 -1
- package/lib/transform-anim.js +1 -1
- package/lib/transition.js +1 -1
- package/lib/validate-nodes.js +227 -0
- 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();
|