voidless-mcp 0.0.0-stage → 1.0.0
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/LICENSE +21 -0
- package/README.md +67 -2
- package/index.js +539 -0
- package/package.json +34 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Voidless Dev Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,68 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Voidless MCP server
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Generate game assets from any MCP client (Claude Desktop, Claude Code, Cursor, VS Code, Windsurf and others): sprites, animations, tilesets, sound effects, music, video and 3D models, saved straight to your [Voidless](https://voidless.dev) library.
|
|
4
|
+
|
|
5
|
+
## Requirements
|
|
6
|
+
|
|
7
|
+
- Node.js 18 or newer
|
|
8
|
+
- A Voidless account on **Voidless+** and an API key (starts with `void_`), created at [voidless.dev/account](https://voidless.dev/account)
|
|
9
|
+
|
|
10
|
+
Every generation spends credits from your Voidless account. Tool descriptions list the cost tiers where they differ.
|
|
11
|
+
|
|
12
|
+
## Setup
|
|
13
|
+
|
|
14
|
+
Claude Desktop / Cursor / any client with a JSON config:
|
|
15
|
+
|
|
16
|
+
```json
|
|
17
|
+
{
|
|
18
|
+
"mcpServers": {
|
|
19
|
+
"voidless": {
|
|
20
|
+
"command": "npx",
|
|
21
|
+
"args": ["-y", "voidless-mcp"],
|
|
22
|
+
"env": { "VOIDLESS_API_KEY": "void_..." }
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Claude Code:
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
claude mcp add voidless -e VOIDLESS_API_KEY=void_... -- npx -y voidless-mcp
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Tools
|
|
35
|
+
|
|
36
|
+
| Tool | What it does |
|
|
37
|
+
| --- | --- |
|
|
38
|
+
| `generate_image` | Sprite, character, portrait, prop, tileset or artwork in a chosen style, palette, size and perspective |
|
|
39
|
+
| `generate_image_edit` | Edit an image with a text instruction |
|
|
40
|
+
| `generate_image_rotate` | Rotated views of a sprite (top-down / isometric angle sheets) |
|
|
41
|
+
| `generate_image_parts` | Split a sprite into parts (head, body, limbs) |
|
|
42
|
+
| `generate_image_animation` | Animate a sprite (walk, idle, ...) |
|
|
43
|
+
| `generate_multi_animation` | Several animations for a character in one pass |
|
|
44
|
+
| `generate_multi_animation_edit` | Edit an animation set with a text instruction |
|
|
45
|
+
| `generate_image_animation_edit` | Edit one frame of an animation |
|
|
46
|
+
| `generate_pixelate_image` | Convert any image to pixel art |
|
|
47
|
+
| `upload_image` | Upload an image to your library |
|
|
48
|
+
| `generate_sfx` | Sound effect from a prompt |
|
|
49
|
+
| `generate_music` | Music track from a prompt |
|
|
50
|
+
| `generate_video` / `generate_video_extend` | Short video clip, or extend one |
|
|
51
|
+
| `generate_3d_model` / `generate_3d_model_rig` / `generate_3d_model_animation` | 3D model, rig it, animate it |
|
|
52
|
+
|
|
53
|
+
Every tool creates new content in your library; none deletes or overwrites existing assets.
|
|
54
|
+
|
|
55
|
+
## Example prompts
|
|
56
|
+
|
|
57
|
+
- "Make a 64px pixel-art knight, side view, then animate a walk cycle."
|
|
58
|
+
- "Generate a top-down grass and dirt tileset in a gameboy palette."
|
|
59
|
+
- "Give me a coin pickup sound effect and a short chiptune loop for the title screen."
|
|
60
|
+
- "Create a low-poly treasure chest 3D model, rig it and animate it opening."
|
|
61
|
+
|
|
62
|
+
## Privacy Policy
|
|
63
|
+
|
|
64
|
+
This server sends your prompts, any images you pass in, and your API key to the Voidless API (api.voidless.dev) to run generations; results are stored in your Voidless account. It collects nothing else and keeps no local data. See the [Voidless Privacy Policy](https://voidless.dev/privacy-policy) for how Voidless collects, uses, stores, shares and retains data, and the [Terms of Service](https://voidless.dev/terms-of-service).
|
|
65
|
+
|
|
66
|
+
## Support
|
|
67
|
+
|
|
68
|
+
contact@voidless.dev · API docs: [voidless.dev/api-docs](https://voidless.dev/api-docs)
|
package/index.js
ADDED
|
@@ -0,0 +1,539 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Voidless MCP Server
|
|
4
|
+
*
|
|
5
|
+
* Exposes the Voidless generation API as MCP tools so any MCP-compatible
|
|
6
|
+
* client (Claude Desktop, etc.) can call image gen, audio gen, game agent,
|
|
7
|
+
* video gen, 3D model gen, and more.
|
|
8
|
+
*
|
|
9
|
+
* Required environment variables:
|
|
10
|
+
* VOIDLESS_API_KEY - void_... API key from your Voidless account
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
14
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
15
|
+
import { z } from "zod";
|
|
16
|
+
|
|
17
|
+
const BASE_URL = "https://api.voidless.dev";
|
|
18
|
+
const CDN_URL = "https://cdn.voidless.dev";
|
|
19
|
+
const APP_URL = "https://voidless.dev";
|
|
20
|
+
const API_KEY = process.env.VOIDLESS_API_KEY || "";
|
|
21
|
+
|
|
22
|
+
if (!API_KEY) {
|
|
23
|
+
console.error("[voidless-mcp] VOIDLESS_API_KEY is not set (expected a void_... key)");
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function apiPost(path, body) {
|
|
28
|
+
const url = `${BASE_URL}/api${path}`;
|
|
29
|
+
const res = await fetch(url, {
|
|
30
|
+
method: "POST",
|
|
31
|
+
headers: {
|
|
32
|
+
"Content-Type": "application/json",
|
|
33
|
+
"x-api-key": API_KEY,
|
|
34
|
+
},
|
|
35
|
+
body: JSON.stringify(body),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
let data;
|
|
39
|
+
const contentType = res.headers.get("content-type") || "";
|
|
40
|
+
try {
|
|
41
|
+
data = contentType.includes("application/json")
|
|
42
|
+
? await res.json()
|
|
43
|
+
: { raw: await res.text() };
|
|
44
|
+
} catch {
|
|
45
|
+
data = { raw: await res.text() };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (!res.ok) {
|
|
49
|
+
const msg =
|
|
50
|
+
data?.error?.message ||
|
|
51
|
+
data?.message ||
|
|
52
|
+
data?.raw ||
|
|
53
|
+
`HTTP ${res.status}`;
|
|
54
|
+
throw new Error(`Voidless API error (${res.status}): ${msg}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return data;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Derive the MIME type from a file path extension.
|
|
61
|
+
function mimeFromPath(filePath) {
|
|
62
|
+
const ext = String(filePath || "").split(".").pop().toLowerCase();
|
|
63
|
+
const map = {
|
|
64
|
+
png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg",
|
|
65
|
+
gif: "image/gif", webp: "image/webp",
|
|
66
|
+
mp3: "audio/mpeg", wav: "audio/wav", ogg: "audio/ogg",
|
|
67
|
+
mp4: "video/mp4", webm: "video/webm",
|
|
68
|
+
glb: "model/gltf-binary", gltf: "model/gltf+json",
|
|
69
|
+
};
|
|
70
|
+
return map[ext] || "application/octet-stream";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Fetch a CDN file and return it as base64.
|
|
74
|
+
async function fetchAsBase64(url) {
|
|
75
|
+
const res = await fetch(url);
|
|
76
|
+
if (!res.ok) throw new Error(`Failed to fetch ${url}: HTTP ${res.status}`);
|
|
77
|
+
const buf = await res.arrayBuffer();
|
|
78
|
+
return Buffer.from(buf).toString("base64");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Format any API response that may contain asset data.
|
|
83
|
+
* - Asset IDs become clickable links: https://voidless.dev/asset/{id}
|
|
84
|
+
* - Image file paths are fetched from the CDN and returned as inline images.
|
|
85
|
+
* - Audio / video / 3D file paths are returned as CDN URLs in the text summary.
|
|
86
|
+
*/
|
|
87
|
+
async function formatResult(data) {
|
|
88
|
+
const content = [];
|
|
89
|
+
|
|
90
|
+
// Collect every asset-like object in the response (top-level or nested).
|
|
91
|
+
const assets = [];
|
|
92
|
+
function collectAssets(obj) {
|
|
93
|
+
if (!obj || typeof obj !== "object") return;
|
|
94
|
+
if (Array.isArray(obj)) { obj.forEach(collectAssets); return; }
|
|
95
|
+
// An asset has at minimum an id and a file path.
|
|
96
|
+
if (obj.id && obj.file) assets.push(obj);
|
|
97
|
+
else Object.values(obj).forEach(collectAssets);
|
|
98
|
+
}
|
|
99
|
+
collectAssets(data);
|
|
100
|
+
|
|
101
|
+
if (assets.length === 0) {
|
|
102
|
+
// No asset — just return the raw JSON.
|
|
103
|
+
content.push({ type: "text", text: JSON.stringify(data, null, 2) });
|
|
104
|
+
return { content };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
for (const asset of assets) {
|
|
108
|
+
const assetLink = `${APP_URL}/asset/${asset.id}`;
|
|
109
|
+
const filePath = String(asset.file || "");
|
|
110
|
+
const cdnUrl = filePath ? `${CDN_URL}/${filePath}` : null;
|
|
111
|
+
const mime = cdnUrl ? mimeFromPath(filePath) : null;
|
|
112
|
+
|
|
113
|
+
const lines = [
|
|
114
|
+
`Asset URL: ${assetLink}`,
|
|
115
|
+
cdnUrl && !mime.startsWith("image/") ? `Media URL: ${cdnUrl}` : null,
|
|
116
|
+
asset.prompt ? `Prompt: ${asset.prompt}` : null,
|
|
117
|
+
asset.type ? `Type: ${asset.type}` : null,
|
|
118
|
+
typeof asset.cost === "number" ? `Cost: ${asset.cost} credits` : null,
|
|
119
|
+
typeof asset.credits === "number" ? `Credits remaining: ${asset.credits}` : null,
|
|
120
|
+
].filter(Boolean);
|
|
121
|
+
content.push({ type: "text", text: lines.join("\n") });
|
|
122
|
+
|
|
123
|
+
// Inline image preview.
|
|
124
|
+
if (cdnUrl && mime.startsWith("image/")) {
|
|
125
|
+
try {
|
|
126
|
+
const b64 = await fetchAsBase64(cdnUrl);
|
|
127
|
+
content.push({ type: "image", data: b64, mimeType: mime });
|
|
128
|
+
} catch {
|
|
129
|
+
content.push({ type: "text", text: `Image preview: ${cdnUrl}` });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return { content };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const server = new McpServer({
|
|
138
|
+
name: "voidless",
|
|
139
|
+
version: "1.0.0",
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// Tool annotations (MCP ToolAnnotations). Every tool here creates something new in the
|
|
143
|
+
// caller's Voidless library and spends credits: none only reads, none deletes or overwrites
|
|
144
|
+
// (edits append new frames/assets; the source asset is kept), a repeat call makes a second
|
|
145
|
+
// asset and bills again (not idempotent), and the effects stay inside the user's own
|
|
146
|
+
// Voidless account (closed world). Directories such as Claude's require title +
|
|
147
|
+
// readOnlyHint/destructiveHint on every tool.
|
|
148
|
+
function writeTool(title) {
|
|
149
|
+
return {
|
|
150
|
+
title,
|
|
151
|
+
readOnlyHint: false,
|
|
152
|
+
destructiveHint: false,
|
|
153
|
+
idempotentHint: false,
|
|
154
|
+
openWorldHint: false,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
// Image generation
|
|
160
|
+
// POST /api/generate/image
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
server.tool(
|
|
163
|
+
"generate_image",
|
|
164
|
+
"Generate a game sprite/asset image. Infer every field from the user's request: the art STYLE, " +
|
|
165
|
+
"COLOUR PALETTE, asset TYPE, output SIZE, and PERSPECTIVE (dimension). For characters also set " +
|
|
166
|
+
"facing/pose/bodyType. Returns an asset object with the image URL.",
|
|
167
|
+
{
|
|
168
|
+
prompt: z.string().max(300).describe("What to generate — the subject only (max 300 chars). Do NOT put style/colour words here; use the style/palette fields."),
|
|
169
|
+
model: z
|
|
170
|
+
.enum([
|
|
171
|
+
"flux2-klein",
|
|
172
|
+
"grok",
|
|
173
|
+
"flux-dev",
|
|
174
|
+
"retro-diffusion",
|
|
175
|
+
"seedream3",
|
|
176
|
+
"z-image",
|
|
177
|
+
"p-image",
|
|
178
|
+
"qwen",
|
|
179
|
+
"juggernaut-flux",
|
|
180
|
+
"kontext",
|
|
181
|
+
"imagen4",
|
|
182
|
+
"sana",
|
|
183
|
+
"hidream-i1",
|
|
184
|
+
"ideogram3",
|
|
185
|
+
"gpt-image-2.5",
|
|
186
|
+
"seedream-5-flash",
|
|
187
|
+
])
|
|
188
|
+
.describe("Image model. Use 'retro-diffusion' for true 8/16-bit pixel art; 'flux2-klein' or 'imagen4' for high-res/smooth art."),
|
|
189
|
+
type: z
|
|
190
|
+
.enum(["object", "character", "character portrait", "artwork", "tileset"])
|
|
191
|
+
.describe("Asset type. 'character' = full-body sprite (uses facing/pose/bodyType); 'character portrait' = headshot; 'object'/'item' = props; 'artwork' = wallpaper/scene."),
|
|
192
|
+
dimension: z
|
|
193
|
+
.enum(["2d", "top down", "isometric"])
|
|
194
|
+
.describe("Perspective. '2d' = flat side view, 'top down' = overhead RPG map, 'isometric' = 2.5D."),
|
|
195
|
+
size: z
|
|
196
|
+
.union([z.literal(32), z.literal(64), z.literal(128), z.literal(512)])
|
|
197
|
+
.describe("Output px. Use 32/64/128 for pixel-art style (64 default); use 512 for every non-pixel style. Sizes <512 force a pixel-art look."),
|
|
198
|
+
colours: z.number().int().min(4).max(12).default(8).describe("Colour count for pixel-art palette reduction (4–12)."),
|
|
199
|
+
style: z
|
|
200
|
+
.string()
|
|
201
|
+
.default("")
|
|
202
|
+
.describe(
|
|
203
|
+
"Art style preset — pick one: pixel-art, cartoon, cel-shading, flat, hand-drawn, paper-cutout, " +
|
|
204
|
+
"comic-book, anime, watercolor, low-poly, 3d-render, claymation, photorealistic, voxel. " +
|
|
205
|
+
"Leave empty to auto-pick from size. Pixel-art style requires size 32/64/128."
|
|
206
|
+
),
|
|
207
|
+
palette: z
|
|
208
|
+
.enum([
|
|
209
|
+
"none",
|
|
210
|
+
"black-and-white",
|
|
211
|
+
"retro",
|
|
212
|
+
"vibrant",
|
|
213
|
+
"pastel",
|
|
214
|
+
"neon",
|
|
215
|
+
"earthy",
|
|
216
|
+
"gameboy",
|
|
217
|
+
"sepia",
|
|
218
|
+
])
|
|
219
|
+
.default("none")
|
|
220
|
+
.describe("Colour-palette preset that recolours the result (independent of style). 'none' keeps the style's own colours. Use 'black-and-white' for monochrome."),
|
|
221
|
+
facing: z.enum(["forward", "right"]).default("forward").describe("Character only: 'forward' (front view) or 'right' (side profile)."),
|
|
222
|
+
pose: z.enum(["idle", "tpose"]).default("idle").describe("Character only: 'idle' relaxed stance or 'tpose' rig/reference stance."),
|
|
223
|
+
bodyType: z.enum(["humanoid", "quadruped"]).default("humanoid").describe("Character only: body plan."),
|
|
224
|
+
seed: z.number().int().min(0).default(0).describe("RNG seed (0 = random)"),
|
|
225
|
+
referenceImage: z.string().default("").describe("Base64-encoded reference image (optional)"),
|
|
226
|
+
styleImage: z.string().default("").describe("Base64-encoded style image (optional)"),
|
|
227
|
+
styleId: z.number().int().optional().describe("Saved style ID (optional)"),
|
|
228
|
+
customPalette: z.array(z.string()).default([]).describe("Optional explicit hex colours to quantize to (rarely needed; prefer the 'palette' preset)."),
|
|
229
|
+
},
|
|
230
|
+
writeTool("Generate image"),
|
|
231
|
+
async (args) => {
|
|
232
|
+
// The image API takes character extras + the colour-palette preset inside an
|
|
233
|
+
// `options` bag; everything else stays top-level. `customPalette` maps to the
|
|
234
|
+
// legacy hex-array `palette` field.
|
|
235
|
+
const { facing, pose, bodyType, palette, customPalette, ...rest } = args;
|
|
236
|
+
const body = {
|
|
237
|
+
...rest,
|
|
238
|
+
palette: customPalette,
|
|
239
|
+
options: { facing, pose, bodyType, palette },
|
|
240
|
+
};
|
|
241
|
+
return formatResult(await apiPost("/generate/image", body));
|
|
242
|
+
}
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
// ---------------------------------------------------------------------------
|
|
247
|
+
// Image edit (inpainting / outpainting style edit)
|
|
248
|
+
// POST /api/generate/image-edit
|
|
249
|
+
// ---------------------------------------------------------------------------
|
|
250
|
+
server.tool(
|
|
251
|
+
"generate_image_edit",
|
|
252
|
+
"Edit an existing image with a text prompt using an AI model.",
|
|
253
|
+
{
|
|
254
|
+
prompt: z.string().describe("Edit instruction"),
|
|
255
|
+
image: z.string().describe("Base64-encoded source image"),
|
|
256
|
+
model: z.string().default("klein").describe("Edit model"),
|
|
257
|
+
},
|
|
258
|
+
writeTool("Edit image"),
|
|
259
|
+
async (args) => formatResult(await apiPost("/generate/image-edit", args))
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
// ---------------------------------------------------------------------------
|
|
264
|
+
// Image edit — rotate (multi-angle sprite sheet)
|
|
265
|
+
// POST /api/generate/image-rotations
|
|
266
|
+
// ---------------------------------------------------------------------------
|
|
267
|
+
server.tool(
|
|
268
|
+
"generate_image_rotate",
|
|
269
|
+
"Generate rotated views of a sprite/asset (used for top-down or isometric angle sheets).",
|
|
270
|
+
{
|
|
271
|
+
id: z.number().int().describe("Asset ID to rotate"),
|
|
272
|
+
mode: z
|
|
273
|
+
.enum(["basic", "basic2"])
|
|
274
|
+
.describe("basic = fast low-quality, basic2 = high-quality"),
|
|
275
|
+
referenceFile: z.string().optional().describe("Frame filename to use as the base (optional)"),
|
|
276
|
+
},
|
|
277
|
+
writeTool("Generate rotated views"),
|
|
278
|
+
async (args) => formatResult(await apiPost("/generate/image-rotations", args))
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
// ---------------------------------------------------------------------------
|
|
284
|
+
// Image parts (isolate / extract sprite parts)
|
|
285
|
+
// POST /api/generate/image-parts
|
|
286
|
+
// ---------------------------------------------------------------------------
|
|
287
|
+
server.tool(
|
|
288
|
+
"generate_image_parts",
|
|
289
|
+
"Extract or generate individual parts (head, body, legs, etc.) from a sprite asset.",
|
|
290
|
+
{
|
|
291
|
+
id: z.number().int().describe("Asset ID"),
|
|
292
|
+
referenceFile: z.string().optional().describe("Specific frame to use as source (optional)"),
|
|
293
|
+
},
|
|
294
|
+
writeTool("Extract sprite parts"),
|
|
295
|
+
async (args) => formatResult(await apiPost("/generate/image-parts", args))
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
// ---------------------------------------------------------------------------
|
|
299
|
+
// Image animation
|
|
300
|
+
// POST /api/generate/image-animation
|
|
301
|
+
// ---------------------------------------------------------------------------
|
|
302
|
+
server.tool(
|
|
303
|
+
"generate_image_animation",
|
|
304
|
+
"Animate a static sprite asset to produce walking / idle frames.",
|
|
305
|
+
{
|
|
306
|
+
id: z.number().int().describe("Asset ID to animate"),
|
|
307
|
+
mode: z.enum(["sd", "f2"]).default("sd").describe("sd = standard, f2 = advanced (higher quality, higher cost)"),
|
|
308
|
+
referenceFile: z.string().optional().describe("Frame to animate (optional)"),
|
|
309
|
+
end_frame: z.string().default("").describe("Base64 or filename for the last frame (optional)"),
|
|
310
|
+
},
|
|
311
|
+
writeTool("Animate sprite"),
|
|
312
|
+
async (args) => formatResult(await apiPost("/generate/image-animation", args))
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
// ---------------------------------------------------------------------------
|
|
317
|
+
// Multi-animation (one grid of several animations in a single pass)
|
|
318
|
+
// POST /api/generate/multi-animation
|
|
319
|
+
// ---------------------------------------------------------------------------
|
|
320
|
+
server.tool(
|
|
321
|
+
"generate_multi_animation",
|
|
322
|
+
"Generate several animations for a character in one pass — a grid of clips (walk/idle/attack, " +
|
|
323
|
+
"and/or facing directions) rather than one clip at a time. Prefer this over repeated " +
|
|
324
|
+
"generate_image_animation calls when more than one clip is wanted: it is a single billed job.",
|
|
325
|
+
{
|
|
326
|
+
items: z.array(z.any()).describe("Grid cells to generate — one entry per animation slot"),
|
|
327
|
+
prompt: z.string().describe("What the character should be doing across the set"),
|
|
328
|
+
preset: z.string().optional().describe("Named layout preset (see the presets the app offers)"),
|
|
329
|
+
quality: z
|
|
330
|
+
.enum(["low", "high", "ultra"])
|
|
331
|
+
.default("low")
|
|
332
|
+
.describe(
|
|
333
|
+
"Cost tier, and the widest one here: measured spend is ~$0.15 low, ~$0.45 high, ~$0.90 ultra " +
|
|
334
|
+
"per job. Leave at low unless the result genuinely needs the detail."
|
|
335
|
+
),
|
|
336
|
+
reference: z.boolean().default(false).describe("Use the source asset as a visual reference"),
|
|
337
|
+
holdFacing: z
|
|
338
|
+
.boolean()
|
|
339
|
+
.default(false)
|
|
340
|
+
.describe("End each cell on the frame it started from, so a directional clip cannot drift"),
|
|
341
|
+
slotEnds: z.array(z.any()).default([]).describe("One end frame per grid cell; read in loop mode only"),
|
|
342
|
+
},
|
|
343
|
+
writeTool("Generate animation set"),
|
|
344
|
+
async (args) => formatResult(await apiPost("/generate/multi-animation", args))
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
// ---------------------------------------------------------------------------
|
|
348
|
+
// Multi-animation edit
|
|
349
|
+
// POST /api/generate/multi-animation/edit
|
|
350
|
+
// ---------------------------------------------------------------------------
|
|
351
|
+
server.tool(
|
|
352
|
+
"generate_multi_animation_edit",
|
|
353
|
+
"Edit an existing multi-animation set with a text instruction, across its cells.",
|
|
354
|
+
{
|
|
355
|
+
items: z.array(z.any()).describe("The cells to edit"),
|
|
356
|
+
prompt: z.string().describe("Edit instruction applied to the set"),
|
|
357
|
+
quality: z.enum(["low", "high", "ultra"]).default("low").describe("Cost tier — see generate_multi_animation"),
|
|
358
|
+
},
|
|
359
|
+
writeTool("Edit animation set"),
|
|
360
|
+
async (args) => formatResult(await apiPost("/generate/multi-animation/edit", args))
|
|
361
|
+
);
|
|
362
|
+
|
|
363
|
+
// ---------------------------------------------------------------------------
|
|
364
|
+
// Image animation edit
|
|
365
|
+
// POST /api/generate/image-animation-edit
|
|
366
|
+
// ---------------------------------------------------------------------------
|
|
367
|
+
server.tool(
|
|
368
|
+
"generate_image_animation_edit",
|
|
369
|
+
"Edit a specific frame of an existing animation.",
|
|
370
|
+
{
|
|
371
|
+
id: z.number().int().describe("Asset ID"),
|
|
372
|
+
frameId: z.string().describe("Frame identifier to edit"),
|
|
373
|
+
prompt: z.string().describe("Edit instruction for this frame"),
|
|
374
|
+
},
|
|
375
|
+
writeTool("Edit animation frame"),
|
|
376
|
+
async (args) => formatResult(await apiPost("/generate/image-animation-edit", args))
|
|
377
|
+
);
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
// ---------------------------------------------------------------------------
|
|
383
|
+
// Image pixelation
|
|
384
|
+
// POST /api/pixelate/image
|
|
385
|
+
// ---------------------------------------------------------------------------
|
|
386
|
+
server.tool(
|
|
387
|
+
"generate_pixelate_image",
|
|
388
|
+
"Convert any image into pixel art at a target resolution and colour count.",
|
|
389
|
+
{
|
|
390
|
+
image: z.string().describe("Base64-encoded source image"),
|
|
391
|
+
size: z.union([z.literal(32), z.literal(64), z.literal(128), z.literal(512)]).describe("Target pixel art size"),
|
|
392
|
+
colours: z.number().int().min(4).max(12).describe("Number of colours"),
|
|
393
|
+
},
|
|
394
|
+
writeTool("Pixelate image"),
|
|
395
|
+
async (args) => formatResult(await apiPost("/pixelate/image", args))
|
|
396
|
+
);
|
|
397
|
+
|
|
398
|
+
// ---------------------------------------------------------------------------
|
|
399
|
+
// Image upload
|
|
400
|
+
// POST /api/upload/image
|
|
401
|
+
// ---------------------------------------------------------------------------
|
|
402
|
+
server.tool(
|
|
403
|
+
"upload_image",
|
|
404
|
+
"Upload a raw image to the Voidless asset library.",
|
|
405
|
+
{
|
|
406
|
+
image: z.string().describe("Base64-encoded image data"),
|
|
407
|
+
type: z.string().optional().describe("Asset type label (optional)"),
|
|
408
|
+
},
|
|
409
|
+
writeTool("Upload image"),
|
|
410
|
+
async (args) => formatResult(await apiPost("/upload/image", args))
|
|
411
|
+
);
|
|
412
|
+
|
|
413
|
+
// ---------------------------------------------------------------------------
|
|
414
|
+
// SFX generation
|
|
415
|
+
// POST /api/generate/sfx
|
|
416
|
+
// ---------------------------------------------------------------------------
|
|
417
|
+
server.tool(
|
|
418
|
+
"generate_sfx",
|
|
419
|
+
"Generate a short sound effect from a text prompt.",
|
|
420
|
+
{
|
|
421
|
+
prompt: z.string().describe("Description of the sound effect to generate"),
|
|
422
|
+
},
|
|
423
|
+
writeTool("Generate sound effect"),
|
|
424
|
+
async (args) => formatResult(await apiPost("/generate/sfx", args))
|
|
425
|
+
);
|
|
426
|
+
|
|
427
|
+
// ---------------------------------------------------------------------------
|
|
428
|
+
// Music generation
|
|
429
|
+
// POST /api/generate/music
|
|
430
|
+
// ---------------------------------------------------------------------------
|
|
431
|
+
server.tool(
|
|
432
|
+
"generate_music",
|
|
433
|
+
"Generate a music track from a text prompt.",
|
|
434
|
+
{
|
|
435
|
+
prompt: z.string().describe("Description of the music to generate"),
|
|
436
|
+
mode: z
|
|
437
|
+
.enum(["small", "medium"])
|
|
438
|
+
.default("small")
|
|
439
|
+
.describe("small = short track (10 credits), medium = longer track (20 credits)"),
|
|
440
|
+
},
|
|
441
|
+
writeTool("Generate music"),
|
|
442
|
+
async (args) => formatResult(await apiPost("/generate/music", args))
|
|
443
|
+
);
|
|
444
|
+
|
|
445
|
+
// ---------------------------------------------------------------------------
|
|
446
|
+
// Video generation
|
|
447
|
+
// POST /api/generate/video
|
|
448
|
+
// ---------------------------------------------------------------------------
|
|
449
|
+
server.tool(
|
|
450
|
+
"generate_video",
|
|
451
|
+
"Generate a short video clip from a text prompt.",
|
|
452
|
+
{
|
|
453
|
+
prompt: z.string().describe("Video description"),
|
|
454
|
+
model: z
|
|
455
|
+
.enum(["seedance", "luma ray2flash", "kling1.6", "vidu q1"])
|
|
456
|
+
.default("seedance")
|
|
457
|
+
.describe("Video model (seedance=25cr, luma=35cr, kling=50cr, vidu=75cr)"),
|
|
458
|
+
palette: z.array(z.string()).default([]).describe("Colour palette (optional)"),
|
|
459
|
+
},
|
|
460
|
+
writeTool("Generate video"),
|
|
461
|
+
async (args) => formatResult(await apiPost("/generate/video", args))
|
|
462
|
+
);
|
|
463
|
+
|
|
464
|
+
// ---------------------------------------------------------------------------
|
|
465
|
+
// Video extend
|
|
466
|
+
// POST /api/generate/video-extend
|
|
467
|
+
// ---------------------------------------------------------------------------
|
|
468
|
+
server.tool(
|
|
469
|
+
"generate_video_extend",
|
|
470
|
+
"Extend an existing video asset with additional AI-generated frames.",
|
|
471
|
+
{
|
|
472
|
+
assetId: z.number().int().describe("Asset ID of the video to extend"),
|
|
473
|
+
prompt: z.string().describe("Description of what should happen in the extension"),
|
|
474
|
+
model: z
|
|
475
|
+
.enum(["seedance", "luma ray2flash", "kling1.6", "vidu q1"])
|
|
476
|
+
.default("vidu q1")
|
|
477
|
+
.describe("Model to use for the extension"),
|
|
478
|
+
palette: z.array(z.string()).default([]).describe("Colour palette (optional)"),
|
|
479
|
+
},
|
|
480
|
+
writeTool("Extend video"),
|
|
481
|
+
async (args) => formatResult(await apiPost("/generate/video-extend", args))
|
|
482
|
+
);
|
|
483
|
+
|
|
484
|
+
// ---------------------------------------------------------------------------
|
|
485
|
+
// 3D model generation
|
|
486
|
+
// POST /api/generate/3d-model
|
|
487
|
+
// ---------------------------------------------------------------------------
|
|
488
|
+
server.tool(
|
|
489
|
+
"generate_3d_model",
|
|
490
|
+
"Generate a 3D model from a text prompt.",
|
|
491
|
+
{
|
|
492
|
+
prompt: z.string().describe("Description of the 3D model"),
|
|
493
|
+
mode: z
|
|
494
|
+
.enum(["lq", "hq"])
|
|
495
|
+
.default("hq")
|
|
496
|
+
.describe("lq = low quality / fast (15 credits), hq = high quality (higher cost)"),
|
|
497
|
+
palette: z.array(z.string()).default([]).describe("Colour palette (optional)"),
|
|
498
|
+
},
|
|
499
|
+
writeTool("Generate 3D model"),
|
|
500
|
+
async (args) => formatResult(await apiPost("/generate/3d-model", args))
|
|
501
|
+
);
|
|
502
|
+
|
|
503
|
+
// ---------------------------------------------------------------------------
|
|
504
|
+
// 3D model rigging
|
|
505
|
+
// POST /api/generate/model-rig
|
|
506
|
+
// ---------------------------------------------------------------------------
|
|
507
|
+
server.tool(
|
|
508
|
+
"generate_3d_model_rig",
|
|
509
|
+
"Rig a generated 3D model — add a skeleton so it can be animated. Run this on a model from " +
|
|
510
|
+
"generate_3d_model before generate_3d_model_animation.",
|
|
511
|
+
{
|
|
512
|
+
id: z.number().int().describe("Asset ID of the 3D model to rig"),
|
|
513
|
+
},
|
|
514
|
+
writeTool("Rig 3D model"),
|
|
515
|
+
async (args) => formatResult(await apiPost("/generate/model-rig", args))
|
|
516
|
+
);
|
|
517
|
+
|
|
518
|
+
// ---------------------------------------------------------------------------
|
|
519
|
+
// 3D model animation
|
|
520
|
+
// POST /api/generate/model-animation
|
|
521
|
+
// ---------------------------------------------------------------------------
|
|
522
|
+
server.tool(
|
|
523
|
+
"generate_3d_model_animation",
|
|
524
|
+
"Animate a rigged 3D model from a text prompt. The model must be rigged first " +
|
|
525
|
+
"(generate_3d_model_rig) — an unrigged model has no skeleton to drive.",
|
|
526
|
+
{
|
|
527
|
+
id: z.number().int().describe("Asset ID of the rigged 3D model"),
|
|
528
|
+
prompt: z.string().describe("The motion to generate, e.g. 'walk cycle', 'idle breathing'"),
|
|
529
|
+
},
|
|
530
|
+
writeTool("Animate 3D model"),
|
|
531
|
+
async (args) => formatResult(await apiPost("/generate/model-animation", args))
|
|
532
|
+
);
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
// ---------------------------------------------------------------------------
|
|
536
|
+
// Start
|
|
537
|
+
// ---------------------------------------------------------------------------
|
|
538
|
+
const transport = new StdioServerTransport();
|
|
539
|
+
await server.connect(transport);
|
package/package.json
CHANGED
|
@@ -1,6 +1,36 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "voidless-mcp",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
6
|
-
|
|
3
|
+
"mcpName": "dev.voidless/voidless",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"description": "MCP server for Voidless: generate game sprites, animations, sound, music, video and 3D models",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Voidless Dev Inc. <contact@voidless.dev>",
|
|
8
|
+
"type": "module",
|
|
9
|
+
"bin": {
|
|
10
|
+
"voidless-mcp": "./index.js"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"index.js",
|
|
14
|
+
"README.md",
|
|
15
|
+
"LICENSE"
|
|
16
|
+
],
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=18"
|
|
19
|
+
},
|
|
20
|
+
"homepage": "https://voidless.dev",
|
|
21
|
+
"keywords": [
|
|
22
|
+
"mcp",
|
|
23
|
+
"model-context-protocol",
|
|
24
|
+
"game-assets",
|
|
25
|
+
"sprites",
|
|
26
|
+
"pixel-art",
|
|
27
|
+
"gamedev",
|
|
28
|
+
"ai"
|
|
29
|
+
],
|
|
30
|
+
"scripts": {
|
|
31
|
+
"start": "node index.js"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@modelcontextprotocol/sdk": "^1.12.1"
|
|
35
|
+
}
|
|
36
|
+
}
|