voidless-mcp 1.6.12 → 1.6.14
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 +2 -2
- package/package.json +1 -1
- package/tools.js +77 -25
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
|
|
|
@@ -100,7 +100,7 @@ Or just the hosted connector, no key: `claude mcp add --transport http voidless
|
|
|
100
100
|
| Tool | What it does |
|
|
101
101
|
| --- | --- |
|
|
102
102
|
| `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
|
|
103
|
+
| `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
104
|
| `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
105
|
| `flip_left_side` | Mirror the right-side animations into the left side (walk-right -> walk-left), free |
|
|
106
106
|
|
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.14",
|
|
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.14";
|
|
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
|
|
@@ -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")
|
|
@@ -929,7 +973,8 @@ export function createVoidlessServer(API_KEY) {
|
|
|
929
973
|
server.tool(
|
|
930
974
|
"animate_sprite",
|
|
931
975
|
"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:
|
|
976
|
+
"(walk + run + attack, or every direction) use generate_animation_set instead: one job, " +
|
|
977
|
+
"15 credits per clip. 15 credits.",
|
|
933
978
|
{
|
|
934
979
|
id: assetId,
|
|
935
980
|
prompt: z.string().describe("The motion, e.g. 'walk cycle', 'swings sword overhead'"),
|
|
@@ -950,22 +995,24 @@ export function createVoidlessServer(API_KEY) {
|
|
|
950
995
|
|
|
951
996
|
server.tool(
|
|
952
997
|
"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).
|
|
998
|
+
"Generate several animations in one job, one clip per animation, all run in parallel. Ways to use it:\n" +
|
|
999
|
+
"1. A PRESET on one or more sprites: platformer-character (idle, walk, run, jump, attack-1, death, hurt) or " +
|
|
1000
|
+
"a top-down set (topdown-walk/run/dash/attack: 8 directions; topdown-idle: 4). Every sprite gets every " +
|
|
1001
|
+
"action of the preset.\n" +
|
|
956
1002
|
"2. A PRESET across an 8-way rotation set: one item per facing, {assetId, frameIndex:<rotation frame id>, " +
|
|
957
1003
|
"imageIndex:i} where i is the facing's position in the frame's facings list, and every facing gets its own " +
|
|
958
1004
|
"directional clip (walk-left from the left-facing image, …). If the facings list has no 'down' (the sprite " +
|
|
959
1005
|
"was drawn facing the camera), add {assetId, base:true} as 'down'. If it already has 'down' (a side-view " +
|
|
960
1006
|
"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
|
-
"
|
|
1007
|
+
"pick only the 4 cardinals (down, right, up, left). The left, up-left and down-left clips are mirrored " +
|
|
1008
|
+
"from their right-side twins in the same rotation set, for free; set flipFacings false to animate them too.\n" +
|
|
1009
|
+
"3. No preset, a typed action: one clip per image. A list of actions makes one clip per action (up to 6): " +
|
|
1010
|
+
"'name: description' separated by semicolons names each clip (e.g. 'attack-2: an overhead slash; block: " +
|
|
1011
|
+
"raise the sword to guard'); for one image a plain 'idle, run, jump' also works. Several actions on several " +
|
|
1012
|
+
"images: every image gets every action.\n" +
|
|
1013
|
+
"Cost: 15 credits per clip (mirrored clips are free; clips that fail are refunded). Clips that come back to " +
|
|
1014
|
+
"their start (idle, walk, run, attack, jump, hurt, dash) end on their own first frame, so they loop; one-off " +
|
|
1015
|
+
"clips such as a death do not. Clips are added to their source asset as frames named after the action.",
|
|
969
1016
|
{
|
|
970
1017
|
items: z.array(pick).min(1).max(12).describe("Images to animate, in order"),
|
|
971
1018
|
preset: z
|
|
@@ -973,8 +1020,8 @@ export function createVoidlessServer(API_KEY) {
|
|
|
973
1020
|
.optional()
|
|
974
1021
|
.describe("Named set of actions (see get_animation_presets)"),
|
|
975
1022
|
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 (
|
|
1023
|
+
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)"),
|
|
1024
|
+
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
1025
|
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
1026
|
},
|
|
980
1027
|
writeTool("Generate animation set"),
|
|
@@ -1071,12 +1118,17 @@ export function createVoidlessServer(API_KEY) {
|
|
|
1071
1118
|
|
|
1072
1119
|
server.tool(
|
|
1073
1120
|
"generate_3d_model",
|
|
1074
|
-
"Generate a 3D model (GLB) from a prompt.
|
|
1121
|
+
"Generate a 3D model (GLB) from a prompt. 45 credits (game-ready) or 15 (lq draft).",
|
|
1075
1122
|
{
|
|
1076
1123
|
prompt: z.string().describe("The model to make"),
|
|
1077
1124
|
type: z.enum(["object", "character"]).default("object").describe("'character' makes a T-posed figure ready for rigging"),
|
|
1078
|
-
style: z.string().default("
|
|
1079
|
-
|
|
1125
|
+
style: z.string().default("low-poly").describe(
|
|
1126
|
+
"'low-poly' (default: faceted triangles, flat colours), 'blocky' (voxel art, built from cubes), 'claymation', 'origami', " +
|
|
1127
|
+
"'hand-painted' (stylized fantasy) or 'toy' (glossy vinyl figure)"
|
|
1128
|
+
),
|
|
1129
|
+
mode: z.enum(["game", "lq", "hq"]).default("game").describe(
|
|
1130
|
+
"game = game-ready, a light clean mesh of about 19k triangles (45 credits); lq = draft (15 credits). hq is an old name for game"
|
|
1131
|
+
),
|
|
1080
1132
|
bodyType: z.enum(["humanoid", "quadruped"]).default("humanoid").describe("Characters only"),
|
|
1081
1133
|
seed: z.number().int().min(0).default(0),
|
|
1082
1134
|
},
|