voidless-mcp 1.6.14 → 1.6.16

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.
Files changed (3) hide show
  1. package/README.md +3 -0
  2. package/package.json +1 -1
  3. package/tools.js +120 -9
package/README.md CHANGED
@@ -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 |
@@ -111,6 +113,7 @@ Or just the hosted connector, no key: `claude mcp add --transport http voidless
111
113
  | `generate_sfx` / `generate_music` | Sound effect, or a music track with length, tempo and key |
112
114
  | `generate_video` / `extend_video` | Short clip from a prompt and reference images, or continue one |
113
115
  | `generate_3d_model` / `rig_3d_model` / `animate_3d_model` | 3D model, rig it, animate it |
116
+ | `render_3d_sprites` | A 3D model's move (or the model standing) as 2D sprites: 8, 4, platformer, isometric or front views, 16 frames, 32-512 px; free |
114
117
 
115
118
  Generation tools create new content in your library; none deletes or overwrites existing assets. Each tool's description lists its credit cost.
116
119
 
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.14",
4
+ "version": "1.6.16",
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.14";
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,9 +46,10 @@ 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
+ "render_3d_sprites",
52
53
  ];
53
54
  const GALLERY_HTML = readFileSync(new URL("./gallery.html", import.meta.url), "utf8");
54
55
 
@@ -58,9 +59,10 @@ const GALLERY_HTML = readFileSync(new URL("./gallery.html", import.meta.url), "u
58
59
  // job is still going the tool says so and check_generation fetches the result later (Strapi keeps
59
60
  // it under the key: GET /generate/job/:key).
60
61
  const LONG_PATHS = new Set([
61
- "/generate/image", "/generate/image-edit", "/generate/image-rotations", "/generate/image-parts",
62
+ "/generate/image", "/generate/image-edit", "/generate/bbox-edit", "/generate/image-rotations", "/generate/image-parts",
62
63
  "/generate/image-animation", "/generate/multi-animation", "/generate/video", "/generate/video-extend",
63
64
  "/generate/sfx", "/generate/music", "/generate/3d-model", "/generate/model-rig", "/generate/model-animation",
65
+ "/generate/model-sprites",
64
66
  ]);
65
67
  const WAIT_MS = Number(process.env.VOIDLESS_WAIT_MS) || 80_000;
66
68
  const CUT_OFF = /\((502|504|520|522|524)\)|fetch failed|ECONNRESET|socket|terminated|other side closed/i;
@@ -841,6 +843,65 @@ export function createVoidlessServer(API_KEY) {
841
843
  tool(async (args) => framesResult(await apiPost("/generate/image-edit", { ...args, model: "p-image" }), args.id, "Edit"))
842
844
  );
843
845
 
846
+ server.tool(
847
+ "make_normal_map",
848
+ "Make a normal map for an image asset, for 2D lights in a game engine: which way each pixel faces, so a " +
849
+ "moving light shades it. The shape comes from a depth model, the drawn detail from the picture. PNG at the " +
850
+ "picture's own size, OpenGL style (green = up: Godot and Unity read it as is; flip green for Unreal). " +
851
+ "Pass frameId (from get_asset) for an edit, or an animation (one map per frame); leave it out for the " +
852
+ "image itself. Free.",
853
+ {
854
+ id: assetId,
855
+ frameId: z
856
+ .number()
857
+ .int()
858
+ .optional()
859
+ .describe("Optional: the frame (an edit or an animation, from get_asset) to make it for. Empty = the image itself."),
860
+ },
861
+ writeTool("Make normal map"),
862
+ tool(async ({ id, frameId: fid }) => {
863
+ const data = await apiPost("/generate/normal-map", { id, ...(fid === undefined ? {} : { frameId: fid }) });
864
+ const keys = data.maps?.normals || (data.maps?.normal ? [data.maps.normal] : []);
865
+ const content = [
866
+ {
867
+ type: "text",
868
+ text:
869
+ `Normal map${keys.length > 1 ? `s (${keys.length}, one per frame)` : ""} saved on asset ${id} ` +
870
+ `(${APP_URL}/asset/${id}):\n` + keys.map((k) => `${CDN_URL}/${k}`).join("\n"),
871
+ },
872
+ ];
873
+ const image = keys.length ? await inlineImage(keys[0]) : null;
874
+ if (image) content.push(image);
875
+ return { content, structuredContent: { summary: content[0].text } };
876
+ })
877
+ );
878
+
879
+ server.tool(
880
+ "make_glow_map",
881
+ "Make a glow map from an edit that switched lights on (or made a night version with lights glowing): the " +
882
+ "light the edit added over the picture it came from, on transparency, for an additive or emissive layer in " +
883
+ "a game. Same size as the picture. Pass the edit's frameId (from get_asset). Free.",
884
+ {
885
+ id: assetId,
886
+ frameId: z.number().int().describe("The edit (a frame from get_asset) whose light to keep"),
887
+ },
888
+ writeTool("Make glow map"),
889
+ tool(async ({ id, frameId: fid }) => {
890
+ const data = await apiPost("/generate/glow-map", { id, frameId: fid });
891
+ const keys = data.maps?.glows || (data.maps?.glow ? [data.maps.glow] : []);
892
+ const content = [
893
+ {
894
+ type: "text",
895
+ text: `Glow map${keys.length > 1 ? `s (${keys.length})` : ""} saved on asset ${id} (${APP_URL}/asset/${id}):\n` +
896
+ keys.map((k) => `${CDN_URL}/${k}`).join("\n"),
897
+ },
898
+ ];
899
+ const image = keys.length ? await inlineImage(keys[0]) : null;
900
+ if (image) content.push(image);
901
+ return { content, structuredContent: { summary: content[0].text } };
902
+ })
903
+ );
904
+
844
905
  server.tool(
845
906
  "rotate_sprite",
846
907
  "Turn a sprite into an 8-way set (front, back, sides and diagonals) in one pass. The result is a " +
@@ -1141,11 +1202,10 @@ export function createVoidlessServer(API_KEY) {
1141
1202
 
1142
1203
  server.tool(
1143
1204
  "rig_3d_model",
1144
- "Add a skeleton to a humanoid 3D model so it can be animated (non-humanoids are refused before any " +
1145
- "charge). Run before animate_3d_model. 140 credits.",
1205
+ "Add a skeleton to a 3D model so it can be animated: a humanoid, a four-legged animal, or a prop's " +
1206
+ "moving parts (a chest lid, a door, a tree's trunk). Run before animate_3d_model. 30 credits.",
1146
1207
  {
1147
1208
  id: z.number().int().describe("3D model asset ID"),
1148
- height_meters: z.number().min(0.2).max(10).default(1.7).describe("Character height in metres"),
1149
1209
  },
1150
1210
  writeTool("Rig 3D model"),
1151
1211
  tool(async (args) => {
@@ -1156,11 +1216,18 @@ export function createVoidlessServer(API_KEY) {
1156
1216
 
1157
1217
  server.tool(
1158
1218
  "animate_3d_model",
1159
- "Animate a rigged 3D model from a description of the motion. 30 credits.",
1219
+ "Animate a rigged 3D model. Either a ready-made move (free; characters: idle, walk, run, jump, attack, " +
1220
+ "punch, kick, wave, hurt, death, cast, dance; four-legged animals: idle, walk, run, jump, attack, sit, hurt, " +
1221
+ "death), a move copied from another of the user's rigged models (free: from_id + from_animation), or a " +
1222
+ "description of any motion ('wag its tail', 'open the lid', 20 credits). Returns the clip, a GLB of the " +
1223
+ "skeleton's motion that plays on the rigged model.",
1160
1224
  {
1161
1225
  id: z.number().int().describe("Rigged 3D model asset ID"),
1162
- prompt: z.string().describe("The motion, e.g. 'walk cycle', 'idle breathing'"),
1163
- duration: z.number().min(1).max(10).default(3).describe("Seconds"),
1226
+ preset: z.string().optional().describe("Ready-made move id, e.g. 'walk' (free)"),
1227
+ from_id: z.number().int().optional().describe("Copy a move from this other rigged model (free)"),
1228
+ from_animation: z.number().int().optional().describe("The animation id on from_id to copy"),
1229
+ prompt: z.string().optional().describe("Or describe the motion, e.g. 'a spinning kick' (20 credits)"),
1230
+ duration: z.number().min(1).max(10).optional().describe("Seconds, for a described motion (default: its natural length)"),
1164
1231
  },
1165
1232
  writeTool("Animate 3D model"),
1166
1233
  tool(async (args) => {
@@ -1172,6 +1239,50 @@ export function createVoidlessServer(API_KEY) {
1172
1239
  })
1173
1240
  );
1174
1241
 
1242
+ server.tool(
1243
+ "render_3d_sprites",
1244
+ "Render a 3D model as 2D game sprites: one of its moves (from animate_3d_model) or the model standing, " +
1245
+ "seen from 8 directions, 4, the two sides of a platformer, 4 isometric diagonals, or the front. 16 frames " +
1246
+ "by default, at 32-512 px (pixel art up to 128, from the same pass as generated sprites; smooth above). Saved as an " +
1247
+ "animated sprite asset linked to the model, one animation per direction, ready to export as a sheet or " +
1248
+ "edit. Every render of the same model goes onto that one asset. Free.",
1249
+ {
1250
+ id: z.number().int().describe("3D model asset ID"),
1251
+ animation: z
1252
+ .string()
1253
+ .optional()
1254
+ .describe("The move to render: its name ('walk'), preset or animation id. Empty = the model standing"),
1255
+ directions: z
1256
+ .enum(["eight", "four", "platformer", "isometric", "front"])
1257
+ .default("eight")
1258
+ .describe("Which views: 8 around, 4 (down/right/up/left), platformer (right, left, side-on), isometric diagonals, or the front"),
1259
+ size: z
1260
+ .union([z.literal(32), z.literal(64), z.literal(128), z.literal(256), z.literal(512)])
1261
+ .default(128)
1262
+ .describe("Sprite size in pixels: 32-128 come out as pixel art, 256 and 512 as smooth renders"),
1263
+ frames: z.number().int().min(2).max(32).default(16).describe("Frames per direction (default 16)"),
1264
+ },
1265
+ writeTool("Render 3D sprites"),
1266
+ tool(async (args) => {
1267
+ const data = await apiPost("/generate/model-sprites", args);
1268
+ const asset = await apiGet(`/assets/${data.assetId}`).catch(() => null);
1269
+ const name = String(data.animation?.name || "still").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
1270
+ const made = ((asset?.meta_data?.frames) || [])
1271
+ .filter((f) => f && f.type === "animation" && String(f.name || "").startsWith(`${name}-`))
1272
+ .slice(-(data.directions || []).length || undefined);
1273
+ const head = [
1274
+ `${data.animation ? `"${data.animation.name}"` : "The model standing"} rendered from 3D: ` +
1275
+ `${(data.directions || []).length} direction${(data.directions || []).length === 1 ? "" : "s"} ` +
1276
+ `(${(data.directions || []).join(", ")}), ${data.frameCount} frame${data.frameCount === 1 ? "" : "s"} each at ${data.size} px, ${data.fps} fps.`,
1277
+ `${data.added ? "Added to" : "Saved as"} sprite asset ${data.assetId} (${APP_URL}/asset/${data.assetId}), linked to model ${args.id}.`,
1278
+ money(data),
1279
+ ].filter(Boolean).join("\n");
1280
+ const content = [{ type: "text", text: head }];
1281
+ if (made.length) content.push(...(await frameContent(made)));
1282
+ return withGallery({ content }, made.flatMap((f) => frameItems(f, data.assetId)), "3D sprites");
1283
+ })
1284
+ );
1285
+
1175
1286
  // The inline gallery, and the tools whose results it shows.
1176
1287
  server.registerResource(
1177
1288
  "gallery",