voidless-mcp 1.6.8 → 1.6.10
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 -1
- package/package.json +1 -1
- package/tools.js +66 -13
package/README.md
CHANGED
|
@@ -91,7 +91,8 @@ Or just the hosted connector, no key: `claude mcp add --transport http voidless
|
|
|
91
91
|
| `rotate_sprite` | An 8-way set (front, back, sides, diagonals) in one pass |
|
|
92
92
|
| `extract_parts` | Split a character into body parts for rigging |
|
|
93
93
|
| `upload_image` | Add an image you already have to your library |
|
|
94
|
-
| `pixelate_image` | Convert any image to pixel art |
|
|
94
|
+
| `pixelate_image` | Convert any image to pixel art (32 to 1024 px) |
|
|
95
|
+
| `clean_up_sprite` | Make a sprite or a 1-5 s animation from anywhere game-ready: background removed, pixel art on a real grid, stray pixels fixed; videos come back as 16 frames |
|
|
95
96
|
|
|
96
97
|
**Animation**
|
|
97
98
|
|
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.10",
|
|
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
|
@@ -25,7 +25,7 @@ import { Agent, setGlobalDispatcher } from "undici";
|
|
|
25
25
|
// server does.
|
|
26
26
|
setGlobalDispatcher(new Agent({ headersTimeout: 0, bodyTimeout: 0, connectTimeout: 30_000 }));
|
|
27
27
|
|
|
28
|
-
export const VERSION = "1.6.
|
|
28
|
+
export const VERSION = "1.6.10";
|
|
29
29
|
const BASE_URL = process.env.VOIDLESS_API_URL || "https://api.voidless.dev";
|
|
30
30
|
const CDN_URL = "https://cdn.voidless.dev";
|
|
31
31
|
// 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 GALLERY_MIME = "text/html;profile=mcp-app";
|
|
|
46
46
|
const GALLERY_TOOLS = [
|
|
47
47
|
"generate_image", "upload_image", "edit_image", "rotate_sprite", "animate_sprite", "generate_animation_set",
|
|
48
48
|
"flip_left_side", "generate_video", "generate_sfx", "generate_music", "generate_3d_model", "get_asset",
|
|
49
|
-
"list_assets", "extract_parts", "pixelate_image", "extend_video",
|
|
49
|
+
"list_assets", "extract_parts", "pixelate_image", "extend_video", "clean_up_sprite",
|
|
50
50
|
];
|
|
51
51
|
const GALLERY_HTML = readFileSync(new URL("./gallery.html", import.meta.url), "utf8");
|
|
52
52
|
|
|
@@ -277,9 +277,13 @@ export function createVoidlessServer(API_KEY) {
|
|
|
277
277
|
return item ? [item] : [];
|
|
278
278
|
}
|
|
279
279
|
const MAX_GALLERY = 48;
|
|
280
|
+
// Clients that read structuredContent hand the MODEL that object instead of the text content
|
|
281
|
+
// (Claude Code does), so it carries the same text as `summary`: without it the model saw only the
|
|
282
|
+
// gallery and told users "the tool didn't report its cost" and that it had one frame to look at.
|
|
283
|
+
const textOf = (result) => (result.content || []).filter((c) => c.type === "text").map((c) => c.text).join("\n");
|
|
280
284
|
const withGallery = (result, items, title) => ({
|
|
281
285
|
...result,
|
|
282
|
-
structuredContent: { title, gallery: items.filter(Boolean).slice(0, MAX_GALLERY) },
|
|
286
|
+
structuredContent: { title, summary: textOf(result), gallery: items.filter(Boolean).slice(0, MAX_GALLERY) },
|
|
283
287
|
});
|
|
284
288
|
|
|
285
289
|
// Read by clients as the server's standing instructions (MCP `instructions`).
|
|
@@ -620,9 +624,9 @@ export function createVoidlessServer(API_KEY) {
|
|
|
620
624
|
),
|
|
621
625
|
dimension: z.enum(["2d", "top down", "isometric"]).default("2d").describe("'2d' = side view, 'top down' = overhead, 'isometric' = 2.5D"),
|
|
622
626
|
size: z
|
|
623
|
-
.union([z.literal(32), z.literal(64), z.literal(128), z.literal(512), z.literal(1024)])
|
|
627
|
+
.union([z.literal(32), z.literal(64), z.literal(128), z.literal(256), z.literal(512), z.literal(1024)])
|
|
624
628
|
.default(512)
|
|
625
|
-
.describe("Output px. 32/64/128 give a pixel-art look; 512 for every other style; 1024 for high-res."),
|
|
629
|
+
.describe("Output px. 32/64/128/256 give a pixel-art look; 512 for every other style; 1024 for high-res."),
|
|
626
630
|
colours: z.number().int().min(4).max(12).default(8).describe("Colour count for pixel-art reduction (4–12)"),
|
|
627
631
|
style: z
|
|
628
632
|
.string()
|
|
@@ -721,22 +725,21 @@ export function createVoidlessServer(API_KEY) {
|
|
|
721
725
|
// The ids returned here are off by (parts - 1): the server numbers from the last appended
|
|
722
726
|
// frame. Point at get_asset instead of printing ids that would misdirect a follow-up call.
|
|
723
727
|
const names = (data.frames || []).map((f) => f.name).filter(Boolean);
|
|
724
|
-
return {
|
|
728
|
+
return withGallery({
|
|
725
729
|
content: [
|
|
726
730
|
{
|
|
727
731
|
type: "text",
|
|
728
732
|
text: `Extracted ${names.length} parts from asset ${args.id}: ${names.join(", ")}.\n${money(data)}\nUse get_asset ${args.id} for their frame ids and image keys.`,
|
|
729
733
|
},
|
|
730
734
|
],
|
|
731
|
-
|
|
732
|
-
};
|
|
735
|
+
}, (data.frames || []).flatMap((f) => frameItems(f, args.id)), "Parts");
|
|
733
736
|
})
|
|
734
737
|
);
|
|
735
738
|
|
|
736
739
|
server.tool(
|
|
737
740
|
"upload_image",
|
|
738
741
|
"Add an image the user already has to their library, so it can be animated, rotated or edited. It must " +
|
|
739
|
-
"be square at 32, 64, 128, 512 or 1024 px. Free.",
|
|
742
|
+
"be square at 32, 64, 128, 256, 512 or 1024 px. Free.",
|
|
740
743
|
{
|
|
741
744
|
image: z.string().describe("Base64 image (a data: prefix is stripped automatically)"),
|
|
742
745
|
type: z.enum(["object", "character", "character portrait", "artwork"]).default("character"),
|
|
@@ -755,19 +758,69 @@ export function createVoidlessServer(API_KEY) {
|
|
|
755
758
|
"saved to the library. 1 credit.",
|
|
756
759
|
{
|
|
757
760
|
image: z.string().describe("Base64 source image (a data: prefix is stripped automatically)"),
|
|
758
|
-
size: z.union([z.literal(32), z.literal(64), z.literal(128), z.literal(512)]).default(64)
|
|
761
|
+
size: z.union([z.literal(32), z.literal(64), z.literal(128), z.literal(256), z.literal(512), z.literal(1024)]).default(64)
|
|
762
|
+
.describe("Pixel size. 512 and 1024 are drawn on a real 4x grid (128 / 256 art pixels)"),
|
|
759
763
|
colours: z.number().int().min(4).max(12).default(8),
|
|
760
764
|
},
|
|
761
765
|
writeTool("Pixelate image"),
|
|
762
766
|
tool(async ({ image, ...rest }) => {
|
|
763
767
|
const data = await apiPost("/pixelate/image", { image: String(image).replace(/^data:[^,]*,/, ""), ...rest });
|
|
764
|
-
return {
|
|
768
|
+
return withGallery({
|
|
765
769
|
content: [
|
|
766
770
|
{ type: "text", text: money(data) || "Pixelated." },
|
|
767
771
|
{ type: "image", data: data.image, mimeType: "image/png" },
|
|
768
772
|
],
|
|
769
|
-
|
|
770
|
-
|
|
773
|
+
}, data.image ? [{ kind: "image", url: `data:image/png;base64,${data.image}`, label: "Pixelated" }] : [], "Pixel art");
|
|
774
|
+
})
|
|
775
|
+
);
|
|
776
|
+
|
|
777
|
+
server.tool(
|
|
778
|
+
"clean_up_sprite",
|
|
779
|
+
"Make a sprite or a short animation made ANYWHERE (another AI tool, an artist, a screen recording) " +
|
|
780
|
+
"game-ready with Voidless's own clean-up: background removed with stray pieces checked (sparks and dust " +
|
|
781
|
+
"kept, smudges removed); pixel art put on a real pixel grid with one palette and stray pixels fixed. A 1-5 s " +
|
|
782
|
+
"video comes back as 16 frames: kind 'loop' cuts the cleanest loop (walk, run, idle), 'once' keeps the " +
|
|
783
|
+
"whole move (attack, jump). Saved to the user's library. Give the file as a public URL (best for videos) or " +
|
|
784
|
+
"base64. 1 free run a day (10 on Voidless+), then 1 credit an image, 2 a video. For a quick pixel-art " +
|
|
785
|
+
"version of an already clean image, pixelate_image is enough.",
|
|
786
|
+
{
|
|
787
|
+
url: z.string().optional().describe("Public http(s) link to the image or video. Use this for videos"),
|
|
788
|
+
file: z.string().optional().describe("Base64 file instead of url (a data: prefix is stripped automatically)"),
|
|
789
|
+
kind: z.enum(["image", "video"]).optional().describe("What the file is. Inferred from the url's extension when omitted"),
|
|
790
|
+
style: z.enum(["pixel", "smooth"]).default("pixel").describe("pixel = pixel art at 32-1024; smooth = clean cut-out at 512 or 1024"),
|
|
791
|
+
size: z.union([z.literal(32), z.literal(64), z.literal(128), z.literal(256), z.literal(512), z.literal(1024)]).optional()
|
|
792
|
+
.describe("Output size. Default 64 for pixel, 512 for smooth (smooth allows only 512 or 1024)"),
|
|
793
|
+
motion: z.enum(["loop", "once"]).default("loop").describe("Video only: loop = find the cleanest loop; once = keep the whole clip"),
|
|
794
|
+
},
|
|
795
|
+
writeTool("Clean up sprite"),
|
|
796
|
+
tool(async ({ url, file, kind, style, size, motion }) => {
|
|
797
|
+
if (!url && !file) throw new Error("Give the file as a url or as base64 in file.");
|
|
798
|
+
const pixel = style !== "smooth";
|
|
799
|
+
const out = size || (pixel ? 64 : 512);
|
|
800
|
+
if (!pixel && out !== 512 && out !== 1024) throw new Error("Smooth art comes back at 512 or 1024.");
|
|
801
|
+
const guessed = /\.(mp4|webm|mov|m4v)(\?|$)/i.test(String(url || "")) ? "video"
|
|
802
|
+
: /^data:video\//i.test(String(file || "")) ? "video" : "image";
|
|
803
|
+
const body = { kind: kind || guessed, size: out, pixel, loop: motion };
|
|
804
|
+
if (url) body.url = url;
|
|
805
|
+
else body.data = String(file).replace(/^data:[^,]*,/, "");
|
|
806
|
+
const data = await apiPost("/cleanup/run", body);
|
|
807
|
+
const link = data.assetId ? `${APP_URL}/asset/${data.assetId}` : undefined;
|
|
808
|
+
const lines = [
|
|
809
|
+
`Cleaned up: ${data.kind === "video" ? `${(data.images || []).length} frames` : "1 sprite"} at ${data.size}px${data.pixel ? " pixel art" : ""}.`,
|
|
810
|
+
data.assetId ? `Asset ID: ${data.assetId} · ${link}` : null,
|
|
811
|
+
data.kind === "video" && Array.isArray(data.span) ? `Frames taken from ${data.span[0]}s to ${data.span[1]}s of the clip.` : null,
|
|
812
|
+
data.free ? `Free run (${data.freeLeft} free left today).` : null,
|
|
813
|
+
money(data),
|
|
814
|
+
].filter(Boolean);
|
|
815
|
+
const content = [{ type: "text", text: lines.join("\n") }];
|
|
816
|
+
if (data.kind === "video") {
|
|
817
|
+
const frame = { id: 0, name: "Clean-up", type: "animation", images: data.images || [], gif: data.gif };
|
|
818
|
+
content.push(...(await frameContent([frame])));
|
|
819
|
+
return withGallery({ content }, frameItems(frame, data.assetId), "Clean-up");
|
|
820
|
+
}
|
|
821
|
+
const image = await inlineImage(data.file);
|
|
822
|
+
if (image) content.push(image);
|
|
823
|
+
return withGallery({ content }, [mediaItem(data.file, "Cleaned up", { link })], "Clean-up");
|
|
771
824
|
})
|
|
772
825
|
);
|
|
773
826
|
|