fui-studio-mcp 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -2
- package/dist/{chunk-NDQB3ETD.js → chunk-NYLRWVKF.js} +2241 -250
- package/dist/{chunk-EFZHSV66.js → chunk-ZCNEK4RK.js} +1 -1
- package/dist/{dist-RTISZO3Q.js → dist-3XKU7BN6.js} +1 -1
- package/dist/index.js +167 -7
- package/dist/{node-3VDUDD3U.js → node-VANCEYKL.js} +2 -2
- package/dist/runtime.global.js +2425 -2270
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -4,11 +4,12 @@ import {
|
|
|
4
4
|
allTtfBuffers,
|
|
5
5
|
fontDefs,
|
|
6
6
|
nodeOutliner
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-ZCNEK4RK.js";
|
|
8
8
|
import {
|
|
9
9
|
DEFAULT_FONT_FAMILY,
|
|
10
10
|
Keyframe,
|
|
11
11
|
OgrafControl,
|
|
12
|
+
PRESETS,
|
|
12
13
|
Param,
|
|
13
14
|
ROOT_TYPES,
|
|
14
15
|
adoptions,
|
|
@@ -16,6 +17,7 @@ import {
|
|
|
16
17
|
emptyProject,
|
|
17
18
|
envName,
|
|
18
19
|
evaluate,
|
|
20
|
+
insertComposition,
|
|
19
21
|
isExposable,
|
|
20
22
|
layerDuration,
|
|
21
23
|
layerLoop,
|
|
@@ -38,7 +40,7 @@ import {
|
|
|
38
40
|
wouldCycle,
|
|
39
41
|
yuvFrameBytes,
|
|
40
42
|
yuvToRgb
|
|
41
|
-
} from "./chunk-
|
|
43
|
+
} from "./chunk-NYLRWVKF.js";
|
|
42
44
|
|
|
43
45
|
// src/index.ts
|
|
44
46
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
@@ -782,6 +784,18 @@ var Session = class {
|
|
|
782
784
|
this.placed.clear();
|
|
783
785
|
return this.project;
|
|
784
786
|
}
|
|
787
|
+
/** A preset's document as the open project — a clean start, like newProject. */
|
|
788
|
+
newFromPreset(name) {
|
|
789
|
+
const preset = PRESETS.find((p) => p.name === name);
|
|
790
|
+
if (!preset) fail(`No preset "${name}". The presets: ${PRESETS.map((p) => p.name).join(", ")}.`);
|
|
791
|
+
this.project = preset.build();
|
|
792
|
+
this.path = void 0;
|
|
793
|
+
this.stamp = void 0;
|
|
794
|
+
this.held.clear();
|
|
795
|
+
this.placed.clear();
|
|
796
|
+
this.bumpIds();
|
|
797
|
+
return this.project;
|
|
798
|
+
}
|
|
785
799
|
open(path) {
|
|
786
800
|
const abs = this.documentPath(path);
|
|
787
801
|
if (!existsSync2(abs)) fail(`No file at ${abs}.${this.root ? " list_projects shows what the projects folder holds." : ""}`);
|
|
@@ -821,6 +835,75 @@ var Session = class {
|
|
|
821
835
|
this.stamp = this.mtimeOf(target);
|
|
822
836
|
return target;
|
|
823
837
|
}
|
|
838
|
+
/**
|
|
839
|
+
* The elements in the projects folder: `elements/<name>.tui`, each a
|
|
840
|
+
* layer saved for reuse from the studio (its layers pane) — the shelf
|
|
841
|
+
* the studio's elements button shows. Newest first.
|
|
842
|
+
*/
|
|
843
|
+
listElements() {
|
|
844
|
+
if (!this.root) fail("The server was started without a projects folder (--projects <dir> or FUI_PROJECTS), so there is no elements/ folder to list.");
|
|
845
|
+
const dir = join3(this.root, "elements");
|
|
846
|
+
let entries;
|
|
847
|
+
try {
|
|
848
|
+
entries = readdirSync(dir);
|
|
849
|
+
} catch {
|
|
850
|
+
return [];
|
|
851
|
+
}
|
|
852
|
+
const out = [];
|
|
853
|
+
for (const file of entries) {
|
|
854
|
+
if (!file.endsWith(".tui")) continue;
|
|
855
|
+
const abs = join3(dir, file);
|
|
856
|
+
const at = this.mtimeOf(abs) ?? 0;
|
|
857
|
+
let description = "";
|
|
858
|
+
try {
|
|
859
|
+
const { project } = loadProject(JSON.parse(readFileSync3(abs, "utf8")));
|
|
860
|
+
const layer = project.graph.nodes.find((n) => n.type === "layer");
|
|
861
|
+
const members = project.graph.nodes.filter((n) => layer && n.layer === layer.id);
|
|
862
|
+
const kinds = [...new Set(members.map((n) => n.type))];
|
|
863
|
+
description = `${members.length} node${members.length === 1 ? "" : "s"}${kinds.length > 0 ? ` (${kinds.slice(0, 6).join(", ")}${kinds.length > 6 ? ", \u2026" : ""})` : ""}`;
|
|
864
|
+
} catch {
|
|
865
|
+
description = "unreadable";
|
|
866
|
+
}
|
|
867
|
+
out.push({ name: file.slice(0, -4), description, saved: new Date(at).toISOString(), at });
|
|
868
|
+
}
|
|
869
|
+
out.sort((a, b) => b.at - a.at);
|
|
870
|
+
return out.map(({ at: _at, ...rest }) => rest);
|
|
871
|
+
}
|
|
872
|
+
/**
|
|
873
|
+
* An element into the open project as a new Layer on top (the shared
|
|
874
|
+
* surgery in @fui/schema): its name counted up when taken, wired into the
|
|
875
|
+
* Canvas, its Tokens wires re-bound to this document's Tokens. Its
|
|
876
|
+
* pictures' bytes come along from `elements/<name>.assets/` and are
|
|
877
|
+
* written beside the project on save. The Layer and its drivers have no
|
|
878
|
+
* place here yet: the save lays them out, as it lays out any node added
|
|
879
|
+
* without a position.
|
|
880
|
+
*/
|
|
881
|
+
insertElement(name) {
|
|
882
|
+
if (!this.root) fail("The server was started without a projects folder (--projects <dir> or FUI_PROJECTS), so there is no elements/ folder to insert from.");
|
|
883
|
+
const abs = join3(this.root, "elements", `${name.replace(/\.tui$/i, "")}.tui`);
|
|
884
|
+
if (!existsSync2(abs)) {
|
|
885
|
+
const have = this.listElements().map((e) => e.name);
|
|
886
|
+
fail(`No element "${name}".${have.length > 0 ? ` The elements: ${have.join(", ")}.` : " The elements/ folder is empty \u2014 save a layer as an element in the studio first."}`);
|
|
887
|
+
}
|
|
888
|
+
const { project: doc } = loadProject(JSON.parse(readFileSync3(abs, "utf8")));
|
|
889
|
+
const insertion = insertComposition(this.project, { ...doc, name: name.replace(/\.tui$/i, "") }, makeId, (n) => registry.forNode(n) !== void 0);
|
|
890
|
+
if (!insertion) fail(`${abs} is not an element: it has no Layer in it.`);
|
|
891
|
+
const dir = assetDirFor(abs);
|
|
892
|
+
for (const [key, record] of Object.entries(doc.assets)) {
|
|
893
|
+
if (record.video || this.held.has(key)) continue;
|
|
894
|
+
const found = assetFileIn(dir, key, record);
|
|
895
|
+
if (!found) continue;
|
|
896
|
+
const bytes = new Uint8Array(readFileSync3(found.path));
|
|
897
|
+
this.held.set(key, { href: `data:${record.mime};base64,${Buffer.from(bytes).toString("base64")}`, bytes, mime: record.mime });
|
|
898
|
+
}
|
|
899
|
+
this.project.graph.nodes.push(...insertion.nodes);
|
|
900
|
+
this.project.graph.edges.push(...insertion.edges);
|
|
901
|
+
this.project.groups.push(...insertion.groups);
|
|
902
|
+
this.project.assets = insertion.assets;
|
|
903
|
+
for (const n of insertion.nodes) if (n.layer === void 0) this.placed.add(n.id);
|
|
904
|
+
const layer = insertion.nodes.find((n) => n.id === insertion.layerId);
|
|
905
|
+
return { layer, nodes: insertion.nodes };
|
|
906
|
+
}
|
|
824
907
|
/**
|
|
825
908
|
* The projects folder's documents, loose and one folder deep — the shape
|
|
826
909
|
* the studio's Open dialog shows. Newest first.
|
|
@@ -1323,14 +1406,20 @@ var session = new Session({ root: projectsFolder() });
|
|
|
1323
1406
|
var INSTRUCTIONS = `FUI Studio builds animated HUD and FUI graphics (rings, ticks, readouts, crosshairs, scanning bars) as a node graph that renders to SVG, PNG and DaVinci Resolve titles. These tools edit one open project at a time.
|
|
1324
1407
|
|
|
1325
1408
|
How to work:
|
|
1326
|
-
1. Start: new_project (name, width, height), or list_projects then open_project to continue one.
|
|
1327
|
-
2. Learn the library once: list_node_types, optionally by category. Generators draw shapes; modifiers change them; layout repeats and arranges; drivers (value, lfo, noise, time\u2026) produce numbers; post nodes are pixel effects.
|
|
1328
|
-
3. Build: add_node with params, then connect. Every param is also an input socket: connect(from: "lfo1", to: "ring1", toPort: "radius") makes the radius move. That is how things animate; set_keyframe is the other way
|
|
1409
|
+
1. Start: list_presets and new_from_preset when one is near what is asked (a radar, a dial, a boot sequence, panels in depth) \u2014 a working graph to change beats an empty one. Else new_project (name, width, height), or list_projects then open_project to continue one.
|
|
1410
|
+
2. Learn the library once: list_node_types, optionally by category. Generators draw shapes; modifiers change them; layout repeats and arranges; drivers (value, lfo, noise, time\u2026) produce numbers; post nodes are pixel effects. describe_node(type) tells what each param means, its unit and range \u2014 read it before using a node whose params are not obvious.
|
|
1411
|
+
3. Build: add_node with params, then connect. Every param is also an input socket: connect(from: "lfo1", to: "ring1", toPort: "radius") makes the radius move. That is how things animate; set_keyframe is the other way. A param's long form carries more: {"base": 5, "expr": "sin(t) * 10"} for an expression (t is time; in a node that handles several shapes, i is the shape's index, n the count, v the base value; sin, cos, rad, PI, TAU are there), {"base": 0, "stagger": {"amount": 30}} for a ramp across the shapes (shape i gets base + i \xD7 amount), {"base": 5, "jitter": {"amount": 2, "mode": "frozen"}} for seeded variation.
|
|
1329
1412
|
4. Look: render_preview after each few edits and actually check the picture. Pass t (seconds) to see a moment of the animation.
|
|
1330
1413
|
5. Save: save_project with a bare name ("radar") saves radar.tui in the projects folder. The first time you save, tell the user the full path the save reports, and that the studio shows the project live once they choose that same folder there (folder\u2026 in the toolbar, then Open).
|
|
1331
1414
|
|
|
1415
|
+
Pixels: shapes are vectors until a rasterize node draws them; post nodes (blur, bloom, glitch, depth-of-field\u2026) take and give textures, never shapes. The chain is shapes \u2192 rasterize \u2192 post nodes \u2192 a Layer's "in". A Layer draws one picture among its shapes.
|
|
1416
|
+
|
|
1417
|
+
3D: a transform3d node poses the shapes it gets as a plane under the camera (x right, y down, z away from the camera: positive Pos. Z draws smaller). Plane "shape" with a stagger on posZ or rotY poses an array's copies one by one; Order "depth" draws the nearer over the farther. The canvas's perspective is the lens (0 = orthographic). A Layer's own 3D group poses the whole layer the same way. Strokes stay screen pixels on a plane: put outline-stroke before the pose for strokes that thin with distance. depth-of-field wants the picture on "in" and the same shapes that made it on "depth"; shapes a transform3d posed remember their depth, so pose inside the layer, before rasterize.
|
|
1418
|
+
|
|
1332
1419
|
Keep the graph readable, because the user opens it in the studio. A node whose output is wired to nothing draws straight onto the page. That works, but a page of loose nodes looks like a graph with its wires missing. Gather the parts of one element (a radar's rings, ticks, labels and crosshair) into a Merge node: add_node merge, then connect each part to its "in" port. Merge draws in connection order, so the first wire is at the bottom. Give the Merge a name ("radar", "readout") so the studio shows what it is. Leave position out of add_node: on save, nodes without one are laid out in columns that follow the wires.
|
|
1333
1420
|
|
|
1421
|
+
Elements: list_elements shows layers the user saved for reuse in the studio; insert_element puts one into the project as a new Layer, ready to change. When the user mentions something they have made before (their radar, their readout), look there first.
|
|
1422
|
+
|
|
1334
1423
|
Layers: a Layer node is a self-contained composition with its own page and its own clock. add_node with layer: "<layer id>" puts a node inside it. Arrange Layers in time with add_clip; a document with no clips plays every Layer all the time. list_layers explains the rules.
|
|
1335
1424
|
|
|
1336
1425
|
The user is probably watching. When the user has the same projects folder open in FUI Studio (Choose folder in the browser) with this project loaded, every save_project appears there within a second. So save after each meaningful step, and say what changed. The user can edit in the studio too: if save_project refuses because the file changed, open_project it again, which picks up their edits, redo your change and save. Do not reach for overwrite unless the user asks for it.
|
|
@@ -1436,6 +1525,77 @@ server.registerTool(
|
|
|
1436
1525
|
)
|
|
1437
1526
|
)
|
|
1438
1527
|
);
|
|
1528
|
+
server.registerTool(
|
|
1529
|
+
"describe_node",
|
|
1530
|
+
{
|
|
1531
|
+
description: "One node type in full: what every param means, its unit, range, group and choices, and which inputs it takes. Read this before using a node whose params are not obvious \u2014 the 3D nodes, the post effects, the arrays.",
|
|
1532
|
+
inputSchema: { type: z2.string() }
|
|
1533
|
+
},
|
|
1534
|
+
guard(({ type }) => {
|
|
1535
|
+
const d = registry.list().find((n) => n.type === type && !n.hidden);
|
|
1536
|
+
if (!d) throw new SessionError(`No node type "${type}". list_node_types has every type.`);
|
|
1537
|
+
return text({
|
|
1538
|
+
type: d.type,
|
|
1539
|
+
label: d.label,
|
|
1540
|
+
category: d.category,
|
|
1541
|
+
description: d.description,
|
|
1542
|
+
...d.keywords ? { keywords: d.keywords } : {},
|
|
1543
|
+
inputs: d.inputs.map((i) => ({ name: i.name, type: i.type, ...i.multi ? { multi: true } : {}, ...i.accepts ? { accepts: i.accepts } : {} })),
|
|
1544
|
+
outputs: d.outputs.map((o) => ({ name: o.name, type: o.type })),
|
|
1545
|
+
params: d.params.map((p) => ({
|
|
1546
|
+
key: p.key,
|
|
1547
|
+
label: p.label,
|
|
1548
|
+
type: p.type,
|
|
1549
|
+
default: p.default,
|
|
1550
|
+
...p.description ? { description: p.description } : {},
|
|
1551
|
+
...p.group ? { group: p.group } : {},
|
|
1552
|
+
..."unit" in p && p.unit ? { unit: p.unit } : {},
|
|
1553
|
+
..."min" in p && p.min !== void 0 ? { min: p.min } : {},
|
|
1554
|
+
..."max" in p && p.max !== void 0 ? { max: p.max } : {},
|
|
1555
|
+
...isExposable(p) ? {} : { wireable: false },
|
|
1556
|
+
..."options" in p ? { options: p.options.map((o) => ({ value: o.value, label: o.label })) } : {}
|
|
1557
|
+
}))
|
|
1558
|
+
});
|
|
1559
|
+
})
|
|
1560
|
+
);
|
|
1561
|
+
server.registerTool(
|
|
1562
|
+
"list_presets",
|
|
1563
|
+
{
|
|
1564
|
+
description: "The starter documents the studio\u2019s Presets menu offers: complete, working graphs, each a worked example of one subsystem (post chain, tokens, drivers, 3D). Start from the nearest one and change it, rather than building from nothing.",
|
|
1565
|
+
inputSchema: {}
|
|
1566
|
+
},
|
|
1567
|
+
guard(() => text(PRESETS.map((p) => ({ name: p.name, label: p.label, description: p.description }))))
|
|
1568
|
+
);
|
|
1569
|
+
server.registerTool(
|
|
1570
|
+
"new_from_preset",
|
|
1571
|
+
{
|
|
1572
|
+
description: "Open a preset as the project, replacing whatever is open \u2014 like new_project, but with a working graph in it. list_nodes then shows what there is to change.",
|
|
1573
|
+
inputSchema: { name: z2.string() }
|
|
1574
|
+
},
|
|
1575
|
+
guard(async ({ name }) => {
|
|
1576
|
+
session.newFromPreset(name);
|
|
1577
|
+
return text(await session.summary());
|
|
1578
|
+
})
|
|
1579
|
+
);
|
|
1580
|
+
server.registerTool(
|
|
1581
|
+
"list_elements",
|
|
1582
|
+
{
|
|
1583
|
+
description: "The elements in the projects folder: layers the user saved for reuse from the studio (a radar, a readout, a panel), each a working composition with a picture. insert_element puts one into the open project as a new layer; prefer that to rebuilding what the user already has.",
|
|
1584
|
+
inputSchema: {}
|
|
1585
|
+
},
|
|
1586
|
+
guard(() => text(session.listElements()))
|
|
1587
|
+
);
|
|
1588
|
+
server.registerTool(
|
|
1589
|
+
"insert_element",
|
|
1590
|
+
{
|
|
1591
|
+
description: "Insert an element (list_elements) into the open project as a new Layer on top, wired into the Canvas, named after the element (counted up when the name is taken), taking the colours of this project\u2019s Style Tokens. Its nodes are ordinary nodes afterwards: list_nodes shows them, set_param changes them. Returns the new Layer and its nodes.",
|
|
1592
|
+
inputSchema: { name: z2.string().describe("The element\u2019s name as list_elements gives it") }
|
|
1593
|
+
},
|
|
1594
|
+
guard(({ name }) => {
|
|
1595
|
+
const { layer, nodes } = session.insertElement(name);
|
|
1596
|
+
return text({ layer: { id: layer.id, name: layer.name, order: layer.params.order }, nodes: nodes.map((n) => ({ id: n.id, type: n.type, ...n.layer ? { layer: n.layer } : {} })) });
|
|
1597
|
+
})
|
|
1598
|
+
);
|
|
1439
1599
|
server.registerTool(
|
|
1440
1600
|
"list_nodes",
|
|
1441
1601
|
{ description: "Every node in the graph, with params and incoming wires.", inputSchema: {} },
|
|
@@ -1630,8 +1790,8 @@ server.registerTool(
|
|
|
1630
1790
|
const abs = session.resolvePath(path);
|
|
1631
1791
|
mkdirSync2(dirname3(abs), { recursive: true });
|
|
1632
1792
|
if (/\.(ograf|drfx)$/i.test(abs)) {
|
|
1633
|
-
const { exportTemplate } = await import("./node-
|
|
1634
|
-
const { packDrfx, packOgraf } = await import("./dist-
|
|
1793
|
+
const { exportTemplate } = await import("./node-VANCEYKL.js");
|
|
1794
|
+
const { packDrfx, packOgraf } = await import("./dist-3XKU7BN6.js");
|
|
1635
1795
|
const template = exportTemplate(session.project, void 0, session.assetResolver());
|
|
1636
1796
|
const errors = template.lint.filter((i) => i.level === "error");
|
|
1637
1797
|
if (errors.length > 0) throw new SessionError(`The manifest has errors: ${errors.map((i) => `${i.where}: ${i.message}`).join("; ")}`);
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
import {
|
|
3
3
|
loadFaces,
|
|
4
4
|
nodeOutliner
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-ZCNEK4RK.js";
|
|
6
6
|
import {
|
|
7
7
|
buildTemplate
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-NYLRWVKF.js";
|
|
9
9
|
|
|
10
10
|
// ../../packages/ograf/dist/node.js
|
|
11
11
|
import { existsSync, readFileSync } from "fs";
|