voidless-mcp 1.0.0 → 1.6.2
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 +68 -20
- package/gallery.html +176 -0
- package/index.js +8 -524
- package/package.json +7 -3
- package/tools.js +863 -0
package/index.js
CHANGED
|
@@ -1,539 +1,23 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* Voidless MCP Server
|
|
3
|
+
* Voidless MCP Server (local, stdio).
|
|
4
4
|
*
|
|
5
|
-
* Exposes the Voidless
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* Exposes the Voidless API as MCP tools for clients that run a local server: Claude Desktop, Claude
|
|
6
|
+
* Code, Cursor, VS Code. The hosted connector at https://mcp.voidless.dev/mcp serves the same tools
|
|
7
|
+
* (tools.js) to clients that take a URL and sign in instead.
|
|
8
8
|
*
|
|
9
9
|
* Required environment variables:
|
|
10
|
-
* VOIDLESS_API_KEY - void_... API key from your Voidless account
|
|
10
|
+
* VOIDLESS_API_KEY - void_... API key from your Voidless account (Voidless+)
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
14
13
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
15
|
-
import {
|
|
14
|
+
import { createVoidlessServer } from "./tools.js";
|
|
16
15
|
|
|
17
|
-
const BASE_URL = "https://api.voidless.dev";
|
|
18
|
-
const CDN_URL = "https://cdn.voidless.dev";
|
|
19
|
-
const APP_URL = "https://voidless.dev";
|
|
20
16
|
const API_KEY = process.env.VOIDLESS_API_KEY || "";
|
|
21
|
-
|
|
22
17
|
if (!API_KEY) {
|
|
23
18
|
console.error("[voidless-mcp] VOIDLESS_API_KEY is not set (expected a void_... key)");
|
|
24
19
|
process.exit(1);
|
|
25
20
|
}
|
|
26
21
|
|
|
27
|
-
|
|
28
|
-
|
|
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);
|
|
22
|
+
const server = createVoidlessServer(API_KEY);
|
|
23
|
+
await server.connect(new StdioServerTransport());
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "voidless-mcp",
|
|
3
3
|
"mcpName": "dev.voidless/voidless",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.6.2",
|
|
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>",
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
},
|
|
12
12
|
"files": [
|
|
13
13
|
"index.js",
|
|
14
|
+
"tools.js",
|
|
15
|
+
"gallery.html",
|
|
14
16
|
"README.md",
|
|
15
17
|
"LICENSE"
|
|
16
18
|
],
|
|
@@ -28,9 +30,11 @@
|
|
|
28
30
|
"ai"
|
|
29
31
|
],
|
|
30
32
|
"scripts": {
|
|
31
|
-
"start": "node index.js"
|
|
33
|
+
"start": "node index.js",
|
|
34
|
+
"start:http": "node http.js"
|
|
32
35
|
},
|
|
33
36
|
"dependencies": {
|
|
34
|
-
"@modelcontextprotocol/sdk": "^1.12.1"
|
|
37
|
+
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
38
|
+
"undici": "^6.29.0"
|
|
35
39
|
}
|
|
36
40
|
}
|