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/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 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.
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 { z } from "zod";
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
- 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);
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.0.0",
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
  }