voidless-mcp 1.6.12 → 1.6.15
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 +4 -2
- package/package.json +1 -1
- package/tools.js +150 -33
package/README.md
CHANGED
|
@@ -74,7 +74,7 @@ Or just the hosted connector, no key: `claude mcp add --transport http voidless
|
|
|
74
74
|
| `list_assets` | Your assets, newest first, filterable by type or tag |
|
|
75
75
|
| `get_asset` | One asset with every frame: animations, 8-way rotations, edits and parts, with the ids and image keys the other tools take |
|
|
76
76
|
| `check_generation` | The result of a long generation (animation sets, video, 3D) that was still running when its tool returned. Free |
|
|
77
|
-
| `get_animation_presets` | The animation-set presets and what each
|
|
77
|
+
| `get_animation_presets` | The animation-set presets and what each clip costs |
|
|
78
78
|
|
|
79
79
|
**Tagging** (free)
|
|
80
80
|
|
|
@@ -89,6 +89,8 @@ Or just the hosted connector, no key: `claude mcp add --transport http voidless
|
|
|
89
89
|
| --- | --- |
|
|
90
90
|
| `generate_image` | Sprite, character, portrait, prop, tileset, UI piece or artwork in a chosen model, style, palette, size and perspective; artwork can be 16:9 for backdrops |
|
|
91
91
|
| `edit_image` | Change an image with an instruction; the edit is added as a new frame |
|
|
92
|
+
| `make_normal_map` | A normal map for an image, an edit or every frame of an animation, for 2D lights in a game engine; free |
|
|
93
|
+
| `make_glow_map` | The light an edit added (lit windows, a night version's lamps) on its own, for an additive glow layer; free |
|
|
92
94
|
| `rotate_sprite` | An 8-way set (front, back, sides, diagonals) in one pass |
|
|
93
95
|
| `extract_parts` | Split a character into body parts for rigging |
|
|
94
96
|
| `upload_image` | Add an image you already have to your library |
|
|
@@ -100,7 +102,7 @@ Or just the hosted connector, no key: `claude mcp add --transport http voidless
|
|
|
100
102
|
| Tool | What it does |
|
|
101
103
|
| --- | --- |
|
|
102
104
|
| `animate_sprite` | One 16-frame clip from a description of the motion |
|
|
103
|
-
| `generate_animation_set` | Several clips in one job: a preset (platformer, top-down walk/run/dash/attack/idle) on one
|
|
105
|
+
| `generate_animation_set` | Several clips in one job, 15 credits each: a preset (platformer, top-down walk/run/dash/attack/idle) on one or more sprites, one directional clip per facing of an 8-way set (the left side mirrored from the right for free; `autoFlip` mirrors any right-only clips after the run), or your own list of actions. Loops end on their first frame |
|
|
104
106
|
| `get_image_data` | An image, or an animation as one sprite strip, as a small data URI to paste into HTML (for sandboxes such as Claude's HTML artifacts that cannot load outside images), with a CSS snippet to play it |
|
|
105
107
|
| `flip_left_side` | Mirror the right-side animations into the left side (walk-right -> walk-left), free |
|
|
106
108
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "voidless-mcp",
|
|
3
3
|
"mcpName": "dev.voidless/voidless",
|
|
4
|
-
"version": "1.6.
|
|
4
|
+
"version": "1.6.15",
|
|
5
5
|
"description": "MCP server for Voidless: generate game sprites, animations, sound, music, video and 3D models",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "Voidless Dev Inc. <contact@voidless.dev>",
|
package/tools.js
CHANGED
|
@@ -27,7 +27,7 @@ import { Agent, setGlobalDispatcher } from "undici";
|
|
|
27
27
|
// server does.
|
|
28
28
|
setGlobalDispatcher(new Agent({ headersTimeout: 0, bodyTimeout: 0, connectTimeout: 30_000 }));
|
|
29
29
|
|
|
30
|
-
export const VERSION = "1.6.
|
|
30
|
+
export const VERSION = "1.6.15";
|
|
31
31
|
const BASE_URL = process.env.VOIDLESS_API_URL || "https://api.voidless.dev";
|
|
32
32
|
const CDN_URL = "https://cdn.voidless.dev";
|
|
33
33
|
// Where asset links point. Results link to the asset's page in the app rather than to raw GIFs: the
|
|
@@ -46,7 +46,7 @@ const MAX_INLINE_IMAGES = 8;
|
|
|
46
46
|
const GALLERY_URI = "ui://voidless/gallery.html";
|
|
47
47
|
const GALLERY_MIME = "text/html;profile=mcp-app";
|
|
48
48
|
const GALLERY_TOOLS = [
|
|
49
|
-
"generate_image", "upload_image", "edit_image", "rotate_sprite", "animate_sprite", "generate_animation_set",
|
|
49
|
+
"generate_image", "upload_image", "edit_image", "make_normal_map", "make_glow_map", "rotate_sprite", "animate_sprite", "generate_animation_set",
|
|
50
50
|
"flip_left_side", "generate_video", "generate_sfx", "generate_music", "generate_3d_model", "get_asset",
|
|
51
51
|
"list_assets", "extract_parts", "pixelate_image", "extend_video", "clean_up_sprite", "check_generation",
|
|
52
52
|
];
|
|
@@ -58,7 +58,7 @@ const GALLERY_HTML = readFileSync(new URL("./gallery.html", import.meta.url), "u
|
|
|
58
58
|
// job is still going the tool says so and check_generation fetches the result later (Strapi keeps
|
|
59
59
|
// it under the key: GET /generate/job/:key).
|
|
60
60
|
const LONG_PATHS = new Set([
|
|
61
|
-
"/generate/image", "/generate/image-edit", "/generate/image-rotations", "/generate/image-parts",
|
|
61
|
+
"/generate/image", "/generate/image-edit", "/generate/bbox-edit", "/generate/image-rotations", "/generate/image-parts",
|
|
62
62
|
"/generate/image-animation", "/generate/multi-animation", "/generate/video", "/generate/video-extend",
|
|
63
63
|
"/generate/sfx", "/generate/music", "/generate/3d-model", "/generate/model-rig", "/generate/model-animation",
|
|
64
64
|
]);
|
|
@@ -73,6 +73,25 @@ class PendingJob extends Error {
|
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
// A finished job whose full result the server no longer has (kept as a summary on its log): what it
|
|
77
|
+
// made, and where to look, instead of a result the tool could format.
|
|
78
|
+
class RestoredJob extends Error {
|
|
79
|
+
constructor(result) {
|
|
80
|
+
super(result?.message || "Finished.");
|
|
81
|
+
this.result = result;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function restoredText(result) {
|
|
86
|
+
const ids = Array.isArray(result?.assetIds) ? result.assetIds : [];
|
|
87
|
+
return [
|
|
88
|
+
`Done. ${result?.message || "The generation finished."}`,
|
|
89
|
+
ids.length ? `Asset${ids.length === 1 ? "" : "s"}: ${ids.join(", ")}` : null,
|
|
90
|
+
Array.isArray(result?.frameIds) && result.frameIds.length ? `New frame ids: ${result.frameIds.join(", ")}` : null,
|
|
91
|
+
"It has already been made and charged: do not run it again.",
|
|
92
|
+
].filter(Boolean).join("\n");
|
|
93
|
+
}
|
|
94
|
+
|
|
76
95
|
export function createVoidlessServer(API_KEY) {
|
|
77
96
|
async function api(method, path, body, headers = {}) {
|
|
78
97
|
const res = await fetch(`${BASE_URL}/api${path}`, {
|
|
@@ -117,6 +136,10 @@ export function createVoidlessServer(API_KEY) {
|
|
|
117
136
|
if (out === null) throw new PendingJob(key, path);
|
|
118
137
|
// The connection was cut but the job may well be running: check, do not report a failure.
|
|
119
138
|
if (out.e) throw CUT_OFF.test(String(out.e.message || out.e)) ? new PendingJob(key, path) : out.e;
|
|
139
|
+
// The server already had this key (a resend): it answers with that job's state instead of
|
|
140
|
+
// running it again -- still going (202), or done but only a summary kept.
|
|
141
|
+
if (out.v && out.v.job === key && out.v.status === "running") throw new PendingJob(key, path);
|
|
142
|
+
if (out.v && out.v.restored) throw new RestoredJob(out.v);
|
|
120
143
|
return out.v;
|
|
121
144
|
} finally {
|
|
122
145
|
clearTimeout(timer);
|
|
@@ -146,6 +169,7 @@ export function createVoidlessServer(API_KEY) {
|
|
|
146
169
|
if (pending.size > 500) pending.delete(pending.keys().next().value);
|
|
147
170
|
return pendingResult(err.key);
|
|
148
171
|
}
|
|
172
|
+
if (err instanceof RestoredJob) return { content: [{ type: "text", text: restoredText(err.result) }] };
|
|
149
173
|
return { isError: true, content: [{ type: "text", text: String(err.message || err) }] };
|
|
150
174
|
}
|
|
151
175
|
};
|
|
@@ -270,11 +294,25 @@ export function createVoidlessServer(API_KEY) {
|
|
|
270
294
|
}
|
|
271
295
|
|
|
272
296
|
// Multi-animation responses: { results: [{ assetId, frames, skipped? }], quality, cost, … }.
|
|
297
|
+
// "2 sprites × 7 clips": one line for the shape of the set.
|
|
298
|
+
function setSummary(data) {
|
|
299
|
+
const made = (data.results || []).filter((r) => !r.skipped && !r.mirrored);
|
|
300
|
+
if (!made.length) return null;
|
|
301
|
+
const perAsset = new Map();
|
|
302
|
+
for (const r of made) perAsset.set(r.assetId, (perAsset.get(r.assetId) || 0) + (r.frames || []).length);
|
|
303
|
+
const counts = [...perAsset.values()];
|
|
304
|
+
const total = counts.reduce((a, n) => a + n, 0);
|
|
305
|
+
const sprites = perAsset.size;
|
|
306
|
+
return counts.every((n) => n === counts[0])
|
|
307
|
+
? `${sprites} sprite${sprites === 1 ? "" : "s"} × ${counts[0]} clip${counts[0] === 1 ? "" : "s"}`
|
|
308
|
+
: `${total} clips on ${sprites} sprite${sprites === 1 ? "" : "s"}`;
|
|
309
|
+
}
|
|
310
|
+
|
|
273
311
|
async function resultsContent(data, label) {
|
|
274
312
|
const content = [
|
|
275
313
|
{
|
|
276
314
|
type: "text",
|
|
277
|
-
text: [label, data.quality ? `Quality: ${data.quality}` : null, money(data)].filter(Boolean).join("\n"),
|
|
315
|
+
text: [label, setSummary(data), data.quality ? `Quality: ${data.quality}` : null, money(data)].filter(Boolean).join("\n"),
|
|
278
316
|
},
|
|
279
317
|
];
|
|
280
318
|
let inlined = 0;
|
|
@@ -554,8 +592,11 @@ export function createVoidlessServer(API_KEY) {
|
|
|
554
592
|
st = await apiGet(`/generate/job/${encodeURIComponent(job)}`);
|
|
555
593
|
} catch (err) {
|
|
556
594
|
if (/\(404\)/.test(String(err.message))) {
|
|
557
|
-
|
|
558
|
-
|
|
595
|
+
// Not a failure: the server has no record of the key, which says nothing about the job.
|
|
596
|
+
return { content: [{ type: "text", text: `The server has no record of job ${job}. That does NOT mean it ` +
|
|
597
|
+
"failed: it may well have finished and been charged. Before running anything again, look for the " +
|
|
598
|
+
"result with list_assets (newest first) or get_asset on the asset it was for, and ask the user " +
|
|
599
|
+
"before re-running the tool." }] };
|
|
559
600
|
}
|
|
560
601
|
throw err;
|
|
561
602
|
}
|
|
@@ -567,6 +608,8 @@ export function createVoidlessServer(API_KEY) {
|
|
|
567
608
|
const msg = st.result?.error?.message || st.result?.message || `HTTP ${st.httpStatus}`;
|
|
568
609
|
return { isError: true, content: [{ type: "text", text: `The generation failed: ${msg}` }] };
|
|
569
610
|
}
|
|
611
|
+
// Finished before a server restart, with only a summary kept: point at the asset.
|
|
612
|
+
if (st.result && st.result.restored) return { content: [{ type: "text", text: restoredText(st.result) }] };
|
|
570
613
|
const p = pending.get(job);
|
|
571
614
|
if (!p) {
|
|
572
615
|
// This connector restarted since the job began: the raw result is all there is.
|
|
@@ -613,18 +656,19 @@ export function createVoidlessServer(API_KEY) {
|
|
|
613
656
|
server.tool(
|
|
614
657
|
"get_animation_presets",
|
|
615
658
|
"List the animation-set presets (platformer-character, topdown-walk/run/dash/attack/idle) with the " +
|
|
616
|
-
"actions each one makes, and the
|
|
659
|
+
"actions each one makes, and the credit cost per clip. Free.",
|
|
617
660
|
{},
|
|
618
661
|
readTool("Get animation presets"),
|
|
619
662
|
tool(async () => {
|
|
620
663
|
const data = await apiGet("/generate/multi-animation/presets");
|
|
621
664
|
const presets = (data.presets || []).map((p) => `- ${p.key} (${p.label}): ${p.actions.join(", ")}`);
|
|
622
|
-
const
|
|
665
|
+
const perClip = Number(data.pricePerClip) > 0 ? Number(data.pricePerClip) : 15;
|
|
623
666
|
return {
|
|
624
667
|
content: [
|
|
625
668
|
{
|
|
626
669
|
type: "text",
|
|
627
|
-
text: `Presets:\n${presets.join("\n")}\n\
|
|
670
|
+
text: `Presets:\n${presets.join("\n")}\n\n${perClip} credits per clip (mirrored left-side clips are free). ` +
|
|
671
|
+
`Up to ${data.maxImages} images per run.`,
|
|
628
672
|
},
|
|
629
673
|
],
|
|
630
674
|
};
|
|
@@ -759,7 +803,7 @@ export function createVoidlessServer(API_KEY) {
|
|
|
759
803
|
aspect: z
|
|
760
804
|
.enum(["1:1", "16:9"])
|
|
761
805
|
.default("1:1")
|
|
762
|
-
.describe("type 'artwork' only: '16:9' for a backdrop, scene or game background (1344x756 at 1024, 672x378 at 512).
|
|
806
|
+
.describe("type 'artwork' at size 512 or 1024 only: '16:9' for a backdrop, scene or game background (1344x756 at 1024, 672x378 at 512; pixel-art styles come out on a 4 px grid, 672x376 at 512). Smaller sizes, Retro Diffusion and everything that is not artwork are always 1:1"),
|
|
763
807
|
uiMode: z
|
|
764
808
|
.enum(["graphic", "title", "text"])
|
|
765
809
|
.default("graphic")
|
|
@@ -797,6 +841,65 @@ export function createVoidlessServer(API_KEY) {
|
|
|
797
841
|
tool(async (args) => framesResult(await apiPost("/generate/image-edit", { ...args, model: "p-image" }), args.id, "Edit"))
|
|
798
842
|
);
|
|
799
843
|
|
|
844
|
+
server.tool(
|
|
845
|
+
"make_normal_map",
|
|
846
|
+
"Make a normal map for an image asset, for 2D lights in a game engine: which way each pixel faces, so a " +
|
|
847
|
+
"moving light shades it. The shape comes from a depth model, the drawn detail from the picture. PNG at the " +
|
|
848
|
+
"picture's own size, OpenGL style (green = up: Godot and Unity read it as is; flip green for Unreal). " +
|
|
849
|
+
"Pass frameId (from get_asset) for an edit, or an animation (one map per frame); leave it out for the " +
|
|
850
|
+
"image itself. Free.",
|
|
851
|
+
{
|
|
852
|
+
id: assetId,
|
|
853
|
+
frameId: z
|
|
854
|
+
.number()
|
|
855
|
+
.int()
|
|
856
|
+
.optional()
|
|
857
|
+
.describe("Optional: the frame (an edit or an animation, from get_asset) to make it for. Empty = the image itself."),
|
|
858
|
+
},
|
|
859
|
+
writeTool("Make normal map"),
|
|
860
|
+
tool(async ({ id, frameId: fid }) => {
|
|
861
|
+
const data = await apiPost("/generate/normal-map", { id, ...(fid === undefined ? {} : { frameId: fid }) });
|
|
862
|
+
const keys = data.maps?.normals || (data.maps?.normal ? [data.maps.normal] : []);
|
|
863
|
+
const content = [
|
|
864
|
+
{
|
|
865
|
+
type: "text",
|
|
866
|
+
text:
|
|
867
|
+
`Normal map${keys.length > 1 ? `s (${keys.length}, one per frame)` : ""} saved on asset ${id} ` +
|
|
868
|
+
`(${APP_URL}/asset/${id}):\n` + keys.map((k) => `${CDN_URL}/${k}`).join("\n"),
|
|
869
|
+
},
|
|
870
|
+
];
|
|
871
|
+
const image = keys.length ? await inlineImage(keys[0]) : null;
|
|
872
|
+
if (image) content.push(image);
|
|
873
|
+
return { content, structuredContent: { summary: content[0].text } };
|
|
874
|
+
})
|
|
875
|
+
);
|
|
876
|
+
|
|
877
|
+
server.tool(
|
|
878
|
+
"make_glow_map",
|
|
879
|
+
"Make a glow map from an edit that switched lights on (or made a night version with lights glowing): the " +
|
|
880
|
+
"light the edit added over the picture it came from, on transparency, for an additive or emissive layer in " +
|
|
881
|
+
"a game. Same size as the picture. Pass the edit's frameId (from get_asset). Free.",
|
|
882
|
+
{
|
|
883
|
+
id: assetId,
|
|
884
|
+
frameId: z.number().int().describe("The edit (a frame from get_asset) whose light to keep"),
|
|
885
|
+
},
|
|
886
|
+
writeTool("Make glow map"),
|
|
887
|
+
tool(async ({ id, frameId: fid }) => {
|
|
888
|
+
const data = await apiPost("/generate/glow-map", { id, frameId: fid });
|
|
889
|
+
const keys = data.maps?.glows || (data.maps?.glow ? [data.maps.glow] : []);
|
|
890
|
+
const content = [
|
|
891
|
+
{
|
|
892
|
+
type: "text",
|
|
893
|
+
text: `Glow map${keys.length > 1 ? `s (${keys.length})` : ""} saved on asset ${id} (${APP_URL}/asset/${id}):\n` +
|
|
894
|
+
keys.map((k) => `${CDN_URL}/${k}`).join("\n"),
|
|
895
|
+
},
|
|
896
|
+
];
|
|
897
|
+
const image = keys.length ? await inlineImage(keys[0]) : null;
|
|
898
|
+
if (image) content.push(image);
|
|
899
|
+
return { content, structuredContent: { summary: content[0].text } };
|
|
900
|
+
})
|
|
901
|
+
);
|
|
902
|
+
|
|
800
903
|
server.tool(
|
|
801
904
|
"rotate_sprite",
|
|
802
905
|
"Turn a sprite into an 8-way set (front, back, sides and diagonals) in one pass. The result is a " +
|
|
@@ -929,7 +1032,8 @@ export function createVoidlessServer(API_KEY) {
|
|
|
929
1032
|
server.tool(
|
|
930
1033
|
"animate_sprite",
|
|
931
1034
|
"Animate one sprite into a single 16-frame clip from a description of the motion. For several clips " +
|
|
932
|
-
"(walk + run + attack, or every direction) use generate_animation_set instead:
|
|
1035
|
+
"(walk + run + attack, or every direction) use generate_animation_set instead: one job, " +
|
|
1036
|
+
"15 credits per clip. 15 credits.",
|
|
933
1037
|
{
|
|
934
1038
|
id: assetId,
|
|
935
1039
|
prompt: z.string().describe("The motion, e.g. 'walk cycle', 'swings sword overhead'"),
|
|
@@ -950,22 +1054,24 @@ export function createVoidlessServer(API_KEY) {
|
|
|
950
1054
|
|
|
951
1055
|
server.tool(
|
|
952
1056
|
"generate_animation_set",
|
|
953
|
-
"Generate several animations in one job.
|
|
954
|
-
"1. A PRESET on one
|
|
955
|
-
"set (topdown-walk/run/dash/attack: 8 directions; topdown-idle: 4).
|
|
1057
|
+
"Generate several animations in one job, one clip per animation, all run in parallel. Ways to use it:\n" +
|
|
1058
|
+
"1. A PRESET on one or more sprites: platformer-character (idle, walk, run, jump, attack-1, death, hurt) or " +
|
|
1059
|
+
"a top-down set (topdown-walk/run/dash/attack: 8 directions; topdown-idle: 4). Every sprite gets every " +
|
|
1060
|
+
"action of the preset.\n" +
|
|
956
1061
|
"2. A PRESET across an 8-way rotation set: one item per facing, {assetId, frameIndex:<rotation frame id>, " +
|
|
957
1062
|
"imageIndex:i} where i is the facing's position in the frame's facings list, and every facing gets its own " +
|
|
958
1063
|
"directional clip (walk-left from the left-facing image, …). If the facings list has no 'down' (the sprite " +
|
|
959
1064
|
"was drawn facing the camera), add {assetId, base:true} as 'down'. If it already has 'down' (a side-view " +
|
|
960
1065
|
"sprite), use only the rotation's own images: the base is the side view, not the front. For topdown-idle " +
|
|
961
|
-
"pick only the 4 cardinals (down, right, up, left)
|
|
962
|
-
"
|
|
963
|
-
"
|
|
964
|
-
"
|
|
965
|
-
"
|
|
966
|
-
"
|
|
967
|
-
"
|
|
968
|
-
"
|
|
1066
|
+
"pick only the 4 cardinals (down, right, up, left). The left, up-left and down-left clips are mirrored " +
|
|
1067
|
+
"from their right-side twins in the same rotation set, for free; set flipFacings false to animate them too.\n" +
|
|
1068
|
+
"3. No preset, a typed action: one clip per image. A list of actions makes one clip per action (up to 6): " +
|
|
1069
|
+
"'name: description' separated by semicolons names each clip (e.g. 'attack-2: an overhead slash; block: " +
|
|
1070
|
+
"raise the sword to guard'); for one image a plain 'idle, run, jump' also works. Several actions on several " +
|
|
1071
|
+
"images: every image gets every action.\n" +
|
|
1072
|
+
"Cost: 15 credits per clip (mirrored clips are free; clips that fail are refunded). Clips that come back to " +
|
|
1073
|
+
"their start (idle, walk, run, attack, jump, hurt, dash) end on their own first frame, so they loop; one-off " +
|
|
1074
|
+
"clips such as a death do not. Clips are added to their source asset as frames named after the action.",
|
|
969
1075
|
{
|
|
970
1076
|
items: z.array(pick).min(1).max(12).describe("Images to animate, in order"),
|
|
971
1077
|
preset: z
|
|
@@ -973,8 +1079,8 @@ export function createVoidlessServer(API_KEY) {
|
|
|
973
1079
|
.optional()
|
|
974
1080
|
.describe("Named set of actions (see get_animation_presets)"),
|
|
975
1081
|
prompt: z.string().default("").describe("Without a preset: the actions to make, as 'name: description' separated by semicolons. With one: optional extra direction for every clip"),
|
|
976
|
-
holdFacing: z.boolean().default(false).describe("End every clip on the frame it started from,
|
|
977
|
-
flipFacings: z.boolean().default(true).describe("On an 8-way rotation set, mirror the left-side clips from the right-side ones instead of animating them (
|
|
1082
|
+
holdFacing: z.boolean().default(false).describe("End every clip on the frame it started from, even one-off clips such as a death (by default loops end on their start and one-offs don't)"),
|
|
1083
|
+
flipFacings: z.boolean().default(true).describe("On an 8-way rotation set, mirror the left-side clips from the right-side ones instead of animating them (mirrored clips are free). One-sided details swap sides, as on the rotation images"),
|
|
978
1084
|
autoFlip: z.boolean().default(false).describe("After the run, mirror every right-side clip on the asset that has no left twin (animate only the right-facing turns, get all eight). Free"),
|
|
979
1085
|
},
|
|
980
1086
|
writeTool("Generate animation set"),
|
|
@@ -1071,12 +1177,17 @@ export function createVoidlessServer(API_KEY) {
|
|
|
1071
1177
|
|
|
1072
1178
|
server.tool(
|
|
1073
1179
|
"generate_3d_model",
|
|
1074
|
-
"Generate a 3D model (GLB) from a prompt.
|
|
1180
|
+
"Generate a 3D model (GLB) from a prompt. 45 credits (game-ready) or 15 (lq draft).",
|
|
1075
1181
|
{
|
|
1076
1182
|
prompt: z.string().describe("The model to make"),
|
|
1077
1183
|
type: z.enum(["object", "character"]).default("object").describe("'character' makes a T-posed figure ready for rigging"),
|
|
1078
|
-
style: z.string().default("
|
|
1079
|
-
|
|
1184
|
+
style: z.string().default("low-poly").describe(
|
|
1185
|
+
"'low-poly' (default: faceted triangles, flat colours), 'blocky' (voxel art, built from cubes), 'claymation', 'origami', " +
|
|
1186
|
+
"'hand-painted' (stylized fantasy) or 'toy' (glossy vinyl figure)"
|
|
1187
|
+
),
|
|
1188
|
+
mode: z.enum(["game", "lq", "hq"]).default("game").describe(
|
|
1189
|
+
"game = game-ready, a light clean mesh of about 19k triangles (45 credits); lq = draft (15 credits). hq is an old name for game"
|
|
1190
|
+
),
|
|
1080
1191
|
bodyType: z.enum(["humanoid", "quadruped"]).default("humanoid").describe("Characters only"),
|
|
1081
1192
|
seed: z.number().int().min(0).default(0),
|
|
1082
1193
|
},
|
|
@@ -1089,11 +1200,10 @@ export function createVoidlessServer(API_KEY) {
|
|
|
1089
1200
|
|
|
1090
1201
|
server.tool(
|
|
1091
1202
|
"rig_3d_model",
|
|
1092
|
-
"Add a skeleton to a
|
|
1093
|
-
"
|
|
1203
|
+
"Add a skeleton to a 3D model so it can be animated: a humanoid, a four-legged animal, or a prop's " +
|
|
1204
|
+
"moving parts (a chest lid, a door, a tree's trunk). Run before animate_3d_model. 30 credits.",
|
|
1094
1205
|
{
|
|
1095
1206
|
id: z.number().int().describe("3D model asset ID"),
|
|
1096
|
-
height_meters: z.number().min(0.2).max(10).default(1.7).describe("Character height in metres"),
|
|
1097
1207
|
},
|
|
1098
1208
|
writeTool("Rig 3D model"),
|
|
1099
1209
|
tool(async (args) => {
|
|
@@ -1104,11 +1214,18 @@ export function createVoidlessServer(API_KEY) {
|
|
|
1104
1214
|
|
|
1105
1215
|
server.tool(
|
|
1106
1216
|
"animate_3d_model",
|
|
1107
|
-
"Animate a rigged 3D model
|
|
1217
|
+
"Animate a rigged 3D model. Either a ready-made move (free; characters: idle, walk, run, jump, attack, " +
|
|
1218
|
+
"punch, kick, wave, hurt, death, cast, dance; four-legged animals: idle, walk, run, jump, attack, sit, hurt, " +
|
|
1219
|
+
"death), a move copied from another of the user's rigged models (free: from_id + from_animation), or a " +
|
|
1220
|
+
"description of any motion ('wag its tail', 'open the lid', 20 credits). Returns the clip, a GLB of the " +
|
|
1221
|
+
"skeleton's motion that plays on the rigged model.",
|
|
1108
1222
|
{
|
|
1109
1223
|
id: z.number().int().describe("Rigged 3D model asset ID"),
|
|
1110
|
-
|
|
1111
|
-
|
|
1224
|
+
preset: z.string().optional().describe("Ready-made move id, e.g. 'walk' (free)"),
|
|
1225
|
+
from_id: z.number().int().optional().describe("Copy a move from this other rigged model (free)"),
|
|
1226
|
+
from_animation: z.number().int().optional().describe("The animation id on from_id to copy"),
|
|
1227
|
+
prompt: z.string().optional().describe("Or describe the motion, e.g. 'a spinning kick' (20 credits)"),
|
|
1228
|
+
duration: z.number().min(1).max(10).optional().describe("Seconds, for a described motion (default: its natural length)"),
|
|
1112
1229
|
},
|
|
1113
1230
|
writeTool("Animate 3D model"),
|
|
1114
1231
|
tool(async (args) => {
|