voidless-mcp 1.6.11 → 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 +79 -26
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,18 +592,24 @@ 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
|
}
|
|
562
603
|
if (st.status === "running") {
|
|
563
|
-
|
|
604
|
+
const where = st.step ? `${st.step}, about ${Math.round((Number(st.fraction) || 0) * 100)}% — ` : "";
|
|
605
|
+
return { content: [{ type: "text", text: `Still running: ${where}${st.seconds}s so far. Check again in about a minute.` }] };
|
|
564
606
|
}
|
|
565
607
|
if (st.status === "failed") {
|
|
566
608
|
const msg = st.result?.error?.message || st.result?.message || `HTTP ${st.httpStatus}`;
|
|
567
609
|
return { isError: true, content: [{ type: "text", text: `The generation failed: ${msg}` }] };
|
|
568
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) }] };
|
|
569
613
|
const p = pending.get(job);
|
|
570
614
|
if (!p) {
|
|
571
615
|
// This connector restarted since the job began: the raw result is all there is.
|
|
@@ -612,18 +656,19 @@ export function createVoidlessServer(API_KEY) {
|
|
|
612
656
|
server.tool(
|
|
613
657
|
"get_animation_presets",
|
|
614
658
|
"List the animation-set presets (platformer-character, topdown-walk/run/dash/attack/idle) with the " +
|
|
615
|
-
"actions each one makes, and the
|
|
659
|
+
"actions each one makes, and the credit cost per clip. Free.",
|
|
616
660
|
{},
|
|
617
661
|
readTool("Get animation presets"),
|
|
618
662
|
tool(async () => {
|
|
619
663
|
const data = await apiGet("/generate/multi-animation/presets");
|
|
620
664
|
const presets = (data.presets || []).map((p) => `- ${p.key} (${p.label}): ${p.actions.join(", ")}`);
|
|
621
|
-
const
|
|
665
|
+
const perClip = Number(data.pricePerClip) > 0 ? Number(data.pricePerClip) : 15;
|
|
622
666
|
return {
|
|
623
667
|
content: [
|
|
624
668
|
{
|
|
625
669
|
type: "text",
|
|
626
|
-
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.`,
|
|
627
672
|
},
|
|
628
673
|
],
|
|
629
674
|
};
|
|
@@ -758,7 +803,7 @@ export function createVoidlessServer(API_KEY) {
|
|
|
758
803
|
aspect: z
|
|
759
804
|
.enum(["1:1", "16:9"])
|
|
760
805
|
.default("1:1")
|
|
761
|
-
.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"),
|
|
762
807
|
uiMode: z
|
|
763
808
|
.enum(["graphic", "title", "text"])
|
|
764
809
|
.default("graphic")
|
|
@@ -928,7 +973,8 @@ export function createVoidlessServer(API_KEY) {
|
|
|
928
973
|
server.tool(
|
|
929
974
|
"animate_sprite",
|
|
930
975
|
"Animate one sprite into a single 16-frame clip from a description of the motion. For several clips " +
|
|
931
|
-
"(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.",
|
|
932
978
|
{
|
|
933
979
|
id: assetId,
|
|
934
980
|
prompt: z.string().describe("The motion, e.g. 'walk cycle', 'swings sword overhead'"),
|
|
@@ -949,22 +995,24 @@ export function createVoidlessServer(API_KEY) {
|
|
|
949
995
|
|
|
950
996
|
server.tool(
|
|
951
997
|
"generate_animation_set",
|
|
952
|
-
"Generate several animations in one job.
|
|
953
|
-
"1. A PRESET on one
|
|
954
|
-
"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" +
|
|
955
1002
|
"2. A PRESET across an 8-way rotation set: one item per facing, {assetId, frameIndex:<rotation frame id>, " +
|
|
956
1003
|
"imageIndex:i} where i is the facing's position in the frame's facings list, and every facing gets its own " +
|
|
957
1004
|
"directional clip (walk-left from the left-facing image, …). If the facings list has no 'down' (the sprite " +
|
|
958
1005
|
"was drawn facing the camera), add {assetId, base:true} as 'down'. If it already has 'down' (a side-view " +
|
|
959
1006
|
"sprite), use only the rotation's own images: the base is the side view, not the front. For topdown-idle " +
|
|
960
|
-
"pick only the 4 cardinals (down, right, up, left)
|
|
961
|
-
"
|
|
962
|
-
"
|
|
963
|
-
"
|
|
964
|
-
"
|
|
965
|
-
"
|
|
966
|
-
"
|
|
967
|
-
"
|
|
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.",
|
|
968
1016
|
{
|
|
969
1017
|
items: z.array(pick).min(1).max(12).describe("Images to animate, in order"),
|
|
970
1018
|
preset: z
|
|
@@ -972,8 +1020,8 @@ export function createVoidlessServer(API_KEY) {
|
|
|
972
1020
|
.optional()
|
|
973
1021
|
.describe("Named set of actions (see get_animation_presets)"),
|
|
974
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"),
|
|
975
|
-
holdFacing: z.boolean().default(false).describe("End every clip on the frame it started from,
|
|
976
|
-
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"),
|
|
977
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"),
|
|
978
1026
|
},
|
|
979
1027
|
writeTool("Generate animation set"),
|
|
@@ -1070,12 +1118,17 @@ export function createVoidlessServer(API_KEY) {
|
|
|
1070
1118
|
|
|
1071
1119
|
server.tool(
|
|
1072
1120
|
"generate_3d_model",
|
|
1073
|
-
"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).",
|
|
1074
1122
|
{
|
|
1075
1123
|
prompt: z.string().describe("The model to make"),
|
|
1076
1124
|
type: z.enum(["object", "character"]).default("object").describe("'character' makes a T-posed figure ready for rigging"),
|
|
1077
|
-
style: z.string().default("
|
|
1078
|
-
|
|
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
|
+
),
|
|
1079
1132
|
bodyType: z.enum(["humanoid", "quadruped"]).default("humanoid").describe("Characters only"),
|
|
1080
1133
|
seed: z.number().int().min(0).default(0),
|
|
1081
1134
|
},
|