voidless-mcp 1.0.0 → 1.1.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/README.md +44 -17
- package/index.js +629 -354
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -33,28 +33,55 @@ claude mcp add voidless -e VOIDLESS_API_KEY=void_... -- npx -y voidless-mcp
|
|
|
33
33
|
|
|
34
34
|
## Tools
|
|
35
35
|
|
|
36
|
+
**Your library** (free, read-only)
|
|
37
|
+
|
|
38
|
+
| Tool | What it does |
|
|
39
|
+
| --- | --- |
|
|
40
|
+
| `list_assets` | Your assets, newest first, filterable by type or tag |
|
|
41
|
+
| `get_asset` | One asset with every frame: animations, 8-way rotations, edits and parts, with the ids and image keys the other tools take |
|
|
42
|
+
| `get_animation_presets` | The animation-set presets and what each pass costs |
|
|
43
|
+
|
|
44
|
+
**Tagging** (free)
|
|
45
|
+
|
|
46
|
+
| Tool | What it does |
|
|
47
|
+
| --- | --- |
|
|
48
|
+
| `tag_asset` | Add, remove or replace an asset's tags |
|
|
49
|
+
| `tag_frame` | Add, remove or replace the tags on one animation, edit or part |
|
|
50
|
+
|
|
51
|
+
**Images**
|
|
52
|
+
|
|
36
53
|
| Tool | What it does |
|
|
37
54
|
| --- | --- |
|
|
38
|
-
| `generate_image` | Sprite, character, portrait, prop, tileset or artwork in a chosen style, palette, size and perspective |
|
|
39
|
-
| `
|
|
40
|
-
| `
|
|
41
|
-
| `
|
|
42
|
-
| `
|
|
43
|
-
| `
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
|
48
|
-
|
|
|
49
|
-
| `
|
|
50
|
-
| `
|
|
51
|
-
| `
|
|
52
|
-
|
|
53
|
-
|
|
55
|
+
| `generate_image` | Sprite, character, portrait, prop, tileset, UI piece or artwork in a chosen model, style, palette, size and perspective |
|
|
56
|
+
| `edit_image` | Change an image with an instruction; the edit is added as a new frame |
|
|
57
|
+
| `rotate_sprite` | An 8-way set (front, back, sides, diagonals) in one pass |
|
|
58
|
+
| `extract_parts` | Split a character into body parts for rigging |
|
|
59
|
+
| `upload_image` | Add an image you already have to your library |
|
|
60
|
+
| `pixelate_image` | Convert any image to pixel art |
|
|
61
|
+
|
|
62
|
+
**Animation**
|
|
63
|
+
|
|
64
|
+
| Tool | What it does |
|
|
65
|
+
| --- | --- |
|
|
66
|
+
| `animate_sprite` | One 16-frame clip from a description of the motion |
|
|
67
|
+
| `generate_animation_set` | Several clips in one job: a preset (platformer, top-down walk/run/dash/attack/idle) on one sprite, or one directional clip per facing of an 8-way set |
|
|
68
|
+
| `edit_animation_set` | Re-generate existing animations with a change |
|
|
69
|
+
| `preview_keep_motion_edit` / `apply_keep_motion_edit` | Change how a character looks across its animations without re-animating them: preview on one frame, then apply to every frame |
|
|
70
|
+
|
|
71
|
+
**Sound, video and 3D**
|
|
72
|
+
|
|
73
|
+
| Tool | What it does |
|
|
74
|
+
| --- | --- |
|
|
75
|
+
| `generate_sfx` / `generate_music` | Sound effect, or a music track with length, tempo and key |
|
|
76
|
+
| `generate_video` / `extend_video` | Short clip from a prompt and reference images, or continue one |
|
|
77
|
+
| `generate_3d_model` / `rig_3d_model` / `animate_3d_model` | 3D model, rig it, animate it |
|
|
78
|
+
|
|
79
|
+
Generation tools create new content in your library; none deletes or overwrites existing assets. Each tool's description lists its credit cost.
|
|
54
80
|
|
|
55
81
|
## Example prompts
|
|
56
82
|
|
|
57
|
-
- "Make a 64px pixel-art knight, side view,
|
|
83
|
+
- "Make a 64px pixel-art knight, side view, turn it into an 8-way set and give it a top-down walk in every direction."
|
|
84
|
+
- "Put red armour on my knight in all its animations without changing the motion."
|
|
58
85
|
- "Generate a top-down grass and dirt tileset in a gameboy palette."
|
|
59
86
|
- "Give me a coin pickup sound effect and a short chiptune loop for the title screen."
|
|
60
87
|
- "Create a low-poly treasure chest 3D model, rig it and animate it opening."
|
package/index.js
CHANGED
|
@@ -2,536 +2,811 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Voidless MCP Server
|
|
4
4
|
*
|
|
5
|
-
* Exposes the Voidless
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* Exposes the Voidless API as MCP tools so any MCP client (Claude, Cursor, …) can find and tag a
|
|
6
|
+
* user's assets and generate new ones: sprites, edits, 8-way rotations, single animations, whole
|
|
7
|
+
* animation sets from presets, "keep motion" edits across animations, sound, music, video and
|
|
8
|
+
* rigged 3D models.
|
|
8
9
|
*
|
|
9
10
|
* Required environment variables:
|
|
10
|
-
* VOIDLESS_API_KEY - void_... API key from your Voidless account
|
|
11
|
+
* VOIDLESS_API_KEY - void_... API key from your Voidless account (Voidless+)
|
|
12
|
+
*
|
|
13
|
+
* Request shapes follow the Strapi controllers in strapi/src/api/generate/controllers. Several
|
|
14
|
+
* fields there are tested with `!== ""`, so an omitted field is NOT the same as an empty one:
|
|
15
|
+
* referenceImage/styleImage/end_frame are always sent, empty when unused.
|
|
11
16
|
*/
|
|
12
17
|
|
|
13
18
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
14
19
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
15
20
|
import { z } from "zod";
|
|
16
21
|
|
|
17
|
-
const
|
|
22
|
+
const VERSION = "1.1.0";
|
|
23
|
+
const BASE_URL = process.env.VOIDLESS_API_URL || "https://api.voidless.dev";
|
|
18
24
|
const CDN_URL = "https://cdn.voidless.dev";
|
|
19
25
|
const APP_URL = "https://voidless.dev";
|
|
20
26
|
const API_KEY = process.env.VOIDLESS_API_KEY || "";
|
|
21
27
|
|
|
28
|
+
// Inline previews are capped so one call that returns 16 animations does not send 16 images.
|
|
29
|
+
const MAX_INLINE_IMAGES = 4;
|
|
30
|
+
|
|
22
31
|
if (!API_KEY) {
|
|
23
32
|
console.error("[voidless-mcp] VOIDLESS_API_KEY is not set (expected a void_... key)");
|
|
24
33
|
process.exit(1);
|
|
25
34
|
}
|
|
26
35
|
|
|
27
|
-
async function
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
"Content-Type": "application/json",
|
|
33
|
-
"x-api-key": API_KEY,
|
|
34
|
-
},
|
|
35
|
-
body: JSON.stringify(body),
|
|
36
|
+
async function api(method, path, body) {
|
|
37
|
+
const res = await fetch(`${BASE_URL}/api${path}`, {
|
|
38
|
+
method,
|
|
39
|
+
headers: { "Content-Type": "application/json", "x-api-key": API_KEY },
|
|
40
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
36
41
|
});
|
|
37
42
|
|
|
38
43
|
let data;
|
|
39
|
-
const
|
|
44
|
+
const text = await res.text();
|
|
40
45
|
try {
|
|
41
|
-
data =
|
|
42
|
-
? await res.json()
|
|
43
|
-
: { raw: await res.text() };
|
|
46
|
+
data = JSON.parse(text);
|
|
44
47
|
} catch {
|
|
45
|
-
data = { raw:
|
|
48
|
+
data = { raw: text };
|
|
46
49
|
}
|
|
47
50
|
|
|
48
51
|
if (!res.ok) {
|
|
49
|
-
const msg =
|
|
50
|
-
data?.error?.message ||
|
|
51
|
-
data?.message ||
|
|
52
|
-
data?.raw ||
|
|
53
|
-
`HTTP ${res.status}`;
|
|
52
|
+
const msg = data?.error?.message || data?.message || data?.raw || `HTTP ${res.status}`;
|
|
54
53
|
throw new Error(`Voidless API error (${res.status}): ${msg}`);
|
|
55
54
|
}
|
|
56
|
-
|
|
57
55
|
return data;
|
|
58
56
|
}
|
|
59
57
|
|
|
60
|
-
|
|
58
|
+
const apiGet = (path) => api("GET", path);
|
|
59
|
+
const apiPost = (path, body) => api("POST", path, body);
|
|
60
|
+
const apiPut = (path, body) => api("PUT", path, body);
|
|
61
|
+
|
|
62
|
+
// Tool errors go back to the model as text it can act on, not as a protocol failure.
|
|
63
|
+
function tool(handler) {
|
|
64
|
+
return async (args) => {
|
|
65
|
+
try {
|
|
66
|
+
return await handler(args);
|
|
67
|
+
} catch (err) {
|
|
68
|
+
return { isError: true, content: [{ type: "text", text: String(err.message || err) }] };
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
// Formatting
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
|
|
61
77
|
function mimeFromPath(filePath) {
|
|
62
78
|
const ext = String(filePath || "").split(".").pop().toLowerCase();
|
|
63
79
|
const map = {
|
|
64
|
-
png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg",
|
|
65
|
-
|
|
66
|
-
mp3: "audio/mpeg", wav: "audio/wav", ogg: "audio/ogg",
|
|
67
|
-
mp4: "video/mp4", webm: "video/webm",
|
|
80
|
+
png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg", gif: "image/gif", webp: "image/webp",
|
|
81
|
+
mp3: "audio/mpeg", wav: "audio/wav", ogg: "audio/ogg", mp4: "video/mp4", webm: "video/webm",
|
|
68
82
|
glb: "model/gltf-binary", gltf: "model/gltf+json",
|
|
69
83
|
};
|
|
70
84
|
return map[ext] || "application/octet-stream";
|
|
71
85
|
}
|
|
72
86
|
|
|
73
|
-
//
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
if (
|
|
77
|
-
|
|
78
|
-
return
|
|
87
|
+
// Stored keys are R2 paths (assets/<uid>/…) or Drive refs (gdrive:…), which have no public URL.
|
|
88
|
+
function cdnUrl(key) {
|
|
89
|
+
if (!key || typeof key !== "string") return null;
|
|
90
|
+
if (/^https?:\/\//.test(key)) return key;
|
|
91
|
+
if (key.startsWith("gdrive:")) return null;
|
|
92
|
+
return `${CDN_URL}/${key.replace(/^\/+/, "")}`;
|
|
79
93
|
}
|
|
80
94
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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);
|
|
95
|
+
async function inlineImage(key) {
|
|
96
|
+
const url = cdnUrl(key);
|
|
97
|
+
if (!url || !mimeFromPath(url).startsWith("image/")) return null;
|
|
98
|
+
try {
|
|
99
|
+
const res = await fetch(url);
|
|
100
|
+
if (!res.ok) return null;
|
|
101
|
+
const data = Buffer.from(await res.arrayBuffer()).toString("base64");
|
|
102
|
+
return { type: "image", data, mimeType: mimeFromPath(url) };
|
|
103
|
+
} catch {
|
|
104
|
+
return null;
|
|
98
105
|
}
|
|
99
|
-
|
|
106
|
+
}
|
|
100
107
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
108
|
+
function money(data) {
|
|
109
|
+
const parts = [];
|
|
110
|
+
if (typeof data?.cost === "number") parts.push(`Cost: ${data.cost} credits`);
|
|
111
|
+
if (typeof data?.credits === "number") parts.push(`Credits left: ${data.credits}`);
|
|
112
|
+
return parts.join(" · ");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// A rotation frame's `tags` field is its facings list ("right,up,left…"), which animation presets
|
|
116
|
+
// read to match each image to a direction. It is not user tagging, and must not be overwritten.
|
|
117
|
+
const isRotation = (frame) => String(frame?.type || "").startsWith("rotations");
|
|
118
|
+
|
|
119
|
+
// One line per frame, carrying everything a follow-up call needs: the frame id (frameIndex /
|
|
120
|
+
// items / frameId elsewhere) and the image keys (referenceFile / end_frame / imageIndex).
|
|
121
|
+
function frameLine(frame) {
|
|
122
|
+
const images = Array.isArray(frame.images) ? frame.images : [];
|
|
123
|
+
const bits = [`frame ${frame.id}`, frame.type || "frame", frame.name ? `"${frame.name}"` : null];
|
|
124
|
+
if (frame.tags) bits.push(`${isRotation(frame) ? "facings" : "tags"}: ${frame.tags}`);
|
|
125
|
+
const imageText =
|
|
126
|
+
images.length <= 8
|
|
127
|
+
? images.map((k, i) => ` [${i}] ${k}`).join("\n")
|
|
128
|
+
: ` ${images.length} images, first ${images[0]}, last ${images[images.length - 1]}`;
|
|
129
|
+
const gif = frame.gif ? `\n preview: ${cdnUrl(frame.gif) || frame.gif}` : "";
|
|
130
|
+
return ` - ${bits.filter(Boolean).join(" · ")}${gif}${images.length ? "\n" + imageText : ""}`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function frameContent(frames, { inline = true } = {}) {
|
|
134
|
+
const content = [];
|
|
135
|
+
if (!frames?.length) return content;
|
|
136
|
+
content.push({ type: "text", text: "New frames:\n" + frames.map(frameLine).join("\n") });
|
|
137
|
+
if (inline) {
|
|
138
|
+
for (const frame of frames.slice(0, MAX_INLINE_IMAGES)) {
|
|
139
|
+
const image = await inlineImage(frame.gif || frame.images?.[0]);
|
|
140
|
+
if (image) content.push(image);
|
|
141
|
+
}
|
|
105
142
|
}
|
|
143
|
+
return content;
|
|
144
|
+
}
|
|
106
145
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
146
|
+
// Responses that create a new asset: { file, assetId, credits, cost, … }.
|
|
147
|
+
async function assetResult(data, label) {
|
|
148
|
+
const url = cdnUrl(data.file);
|
|
149
|
+
const lines = [
|
|
150
|
+
`${label} created.`,
|
|
151
|
+
data.assetId ? `Asset ID: ${data.assetId} · ${APP_URL}/asset/${data.assetId}` : null,
|
|
152
|
+
url && !mimeFromPath(url).startsWith("image/") ? `File: ${url}` : null,
|
|
153
|
+
data.thumbnail ? `Thumbnail: ${cdnUrl(data.thumbnail)}` : null,
|
|
154
|
+
data.seed !== undefined ? `Seed: ${data.seed}` : null,
|
|
155
|
+
money(data),
|
|
156
|
+
].filter(Boolean);
|
|
157
|
+
const content = [{ type: "text", text: lines.join("\n") }];
|
|
158
|
+
const image = await inlineImage(data.file);
|
|
159
|
+
if (image) content.push(image);
|
|
160
|
+
return { content };
|
|
161
|
+
}
|
|
112
162
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
163
|
+
// Responses that append frames to an existing asset: { frames, credits, cost }.
|
|
164
|
+
async function framesResult(data, assetId, label) {
|
|
165
|
+
const content = [
|
|
166
|
+
{
|
|
167
|
+
type: "text",
|
|
168
|
+
text: [`${label} added to asset ${assetId} (${APP_URL}/asset/${assetId}).`, money(data)]
|
|
169
|
+
.filter(Boolean)
|
|
170
|
+
.join("\n"),
|
|
171
|
+
},
|
|
172
|
+
];
|
|
173
|
+
content.push(...(await frameContent(data.frames)));
|
|
174
|
+
if (Array.isArray(data.missing) && data.missing.length) {
|
|
175
|
+
content.push({ type: "text", text: `Skipped (could not be edited): ${JSON.stringify(data.missing)}` });
|
|
176
|
+
}
|
|
177
|
+
return { content };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Multi-animation responses: { results: [{ assetId, frames, skipped? }], quality, cost, … }.
|
|
181
|
+
async function resultsContent(data, label) {
|
|
182
|
+
const content = [
|
|
183
|
+
{
|
|
184
|
+
type: "text",
|
|
185
|
+
text: [label, data.quality ? `Quality: ${data.quality}` : null, money(data)].filter(Boolean).join("\n"),
|
|
186
|
+
},
|
|
187
|
+
];
|
|
188
|
+
let inlined = 0;
|
|
189
|
+
for (const result of data.results || []) {
|
|
190
|
+
if (result.skipped) {
|
|
191
|
+
content.push({ type: "text", text: `Asset ${result.assetId}: skipped${result.error ? ` (${result.error})` : ""}` });
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
content.push({
|
|
195
|
+
type: "text",
|
|
196
|
+
text: `Asset ${result.assetId} (${APP_URL}/asset/${result.assetId}):\n` + (result.frames || []).map(frameLine).join("\n"),
|
|
197
|
+
});
|
|
198
|
+
for (const frame of result.frames || []) {
|
|
199
|
+
if (inlined >= MAX_INLINE_IMAGES) break;
|
|
200
|
+
const image = await inlineImage(frame.gif || frame.images?.[0]);
|
|
201
|
+
if (image) {
|
|
202
|
+
content.push(image);
|
|
203
|
+
inlined++;
|
|
130
204
|
}
|
|
131
205
|
}
|
|
132
206
|
}
|
|
133
|
-
|
|
134
207
|
return { content };
|
|
135
208
|
}
|
|
136
209
|
|
|
137
|
-
const server = new McpServer({
|
|
138
|
-
name: "voidless",
|
|
139
|
-
version: "1.0.0",
|
|
140
|
-
});
|
|
210
|
+
const server = new McpServer({ name: "voidless", version: VERSION });
|
|
141
211
|
|
|
142
|
-
// Tool annotations (MCP ToolAnnotations).
|
|
143
|
-
//
|
|
144
|
-
//
|
|
145
|
-
//
|
|
146
|
-
// Voidless account (closed world). Directories such as Claude's require title +
|
|
212
|
+
// Tool annotations (MCP ToolAnnotations). Generation tools create something new in the caller's
|
|
213
|
+
// Voidless library and spend credits: none deletes or overwrites (edits append new frames; the
|
|
214
|
+
// source is kept), a repeat call makes another result and bills again, and the effects stay
|
|
215
|
+
// inside the user's own account. Directories such as Claude's require title +
|
|
147
216
|
// readOnlyHint/destructiveHint on every tool.
|
|
148
217
|
function writeTool(title) {
|
|
149
|
-
return {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
idempotentHint: false,
|
|
154
|
-
openWorldHint: false,
|
|
155
|
-
};
|
|
218
|
+
return { title, readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false };
|
|
219
|
+
}
|
|
220
|
+
function readTool(title) {
|
|
221
|
+
return { title, readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
|
|
156
222
|
}
|
|
223
|
+
// Tagging changes labels only (no credits), but replace/remove drop existing tags.
|
|
224
|
+
function tagTool(title) {
|
|
225
|
+
return { title, readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const assetId = z.number().int().describe("Asset ID (from list_assets / get_asset or a previous result)");
|
|
229
|
+
const referenceFile = z
|
|
230
|
+
.string()
|
|
231
|
+
.default("")
|
|
232
|
+
.describe(
|
|
233
|
+
"Work from one image of an existing frame instead of the asset's base: pass an image key exactly as " +
|
|
234
|
+
"get_asset lists it. Empty = the asset's current base image."
|
|
235
|
+
);
|
|
236
|
+
const frameId = z
|
|
237
|
+
.number()
|
|
238
|
+
.int()
|
|
239
|
+
.optional()
|
|
240
|
+
.describe("Optional: the frame the new frames hang under in the editor (usually the frame referenceFile came from).");
|
|
241
|
+
|
|
242
|
+
// ---------------------------------------------------------------------------
|
|
243
|
+
// Reading the library
|
|
244
|
+
// ---------------------------------------------------------------------------
|
|
245
|
+
|
|
246
|
+
server.tool(
|
|
247
|
+
"list_assets",
|
|
248
|
+
"List the user's Voidless assets, newest first: id, type, prompt, tags and how many frames/animations " +
|
|
249
|
+
"each has. Use this to find an asset to animate, rotate or edit. Free.",
|
|
250
|
+
{
|
|
251
|
+
type: z.enum(["image", "sfx", "music", "video", "model"]).optional().describe("Only this asset type"),
|
|
252
|
+
tag: z.string().optional().describe("Only assets whose tags contain this text"),
|
|
253
|
+
page: z.number().int().min(1).default(1),
|
|
254
|
+
pageSize: z.number().int().min(1).max(100).default(25),
|
|
255
|
+
},
|
|
256
|
+
readTool("List assets"),
|
|
257
|
+
tool(async ({ type, tag, page, pageSize }) => {
|
|
258
|
+
const q = [];
|
|
259
|
+
if (type) q.push(`filters[$and][0][type]=${type}`);
|
|
260
|
+
if (tag) q.push(`filters[tags][$contains]=${encodeURIComponent(tag)}`);
|
|
261
|
+
q.push("sort=createdAt:desc", `pagination[page]=${page}`, `pagination[pageSize]=${pageSize}`);
|
|
262
|
+
const data = await apiGet(`/assets?${q.join("&")}`);
|
|
263
|
+
const rows = (data.data || []).map((item) => {
|
|
264
|
+
const a = item.attributes || item;
|
|
265
|
+
const frames = (a.meta_data?.frames || []).filter((f) => !f.archived);
|
|
266
|
+
const animations = frames.filter((f) => f.type === "animation").length;
|
|
267
|
+
const kind = a.meta_data?.image_type ? `${a.type}/${a.meta_data.image_type}` : a.type;
|
|
268
|
+
const extra = frames.length ? ` · ${frames.length} frames (${animations} animations)` : "";
|
|
269
|
+
const tags = a.tags ? ` · tags: ${a.tags}` : "";
|
|
270
|
+
return `- ${item.id} · ${kind} · "${String(a.prompt || "").slice(0, 80)}"${tags}${extra}`;
|
|
271
|
+
});
|
|
272
|
+
const p = data.meta?.pagination;
|
|
273
|
+
const footer = p ? `\nPage ${p.page} of ${p.pageCount} (${p.total} assets)` : "";
|
|
274
|
+
return { content: [{ type: "text", text: (rows.join("\n") || "No assets found.") + footer }] };
|
|
275
|
+
})
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
server.tool(
|
|
279
|
+
"get_asset",
|
|
280
|
+
"Show one asset with every frame: frame ids, types (animation, rotations - 8-way, edit - text, layer…), " +
|
|
281
|
+
"names, facings and image keys. Frame ids are what frameIndex/items/frameId take elsewhere; image keys " +
|
|
282
|
+
"are what referenceFile and end_frame take. Free.",
|
|
283
|
+
{ id: assetId, preview: z.boolean().default(true).describe("Include the base image inline") },
|
|
284
|
+
readTool("Get asset"),
|
|
285
|
+
tool(async ({ id, preview }) => {
|
|
286
|
+
const a = await apiGet(`/assets/${id}`);
|
|
287
|
+
const meta = a.meta_data || {};
|
|
288
|
+
const frames = (meta.frames || []).filter((f) => !f.archived);
|
|
289
|
+
const lines = [
|
|
290
|
+
`Asset ${a.id} · ${a.type}${meta.image_type ? "/" + meta.image_type : ""} · ${APP_URL}/asset/${a.id}`,
|
|
291
|
+
a.prompt ? `Prompt: ${a.prompt}` : null,
|
|
292
|
+
`Tags: ${a.tags || "(none)"}`,
|
|
293
|
+
meta.size ? `Size: ${meta.size}px · ${meta.dimension || ""} · style ${meta.style || "-"} · facing ${meta.facing || "-"}` : null,
|
|
294
|
+
a.file ? `Base image: ${a.file}` : null,
|
|
295
|
+
meta.rigged_file ? `Rigged: yes (${(meta.animations || []).length} animations)` : null,
|
|
296
|
+
Array.isArray(meta.video_extensions) && meta.video_extensions.length
|
|
297
|
+
? `Video extensions: ${meta.video_extensions.map((v) => v.file).join(", ")}`
|
|
298
|
+
: null,
|
|
299
|
+
frames.length ? `Frames (${frames.length}):\n${frames.map(frameLine).join("\n")}` : "No frames yet.",
|
|
300
|
+
].filter(Boolean);
|
|
301
|
+
const content = [{ type: "text", text: lines.join("\n") }];
|
|
302
|
+
if (preview) {
|
|
303
|
+
const image = await inlineImage(a.file);
|
|
304
|
+
if (image) content.push(image);
|
|
305
|
+
}
|
|
306
|
+
return { content };
|
|
307
|
+
})
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
server.tool(
|
|
311
|
+
"get_animation_presets",
|
|
312
|
+
"List the animation-set presets (platformer-character, topdown-walk/run/dash/attack/idle) with the " +
|
|
313
|
+
"actions each one makes, and the quality tiers with their credit cost per pass. Free.",
|
|
314
|
+
{},
|
|
315
|
+
readTool("Get animation presets"),
|
|
316
|
+
tool(async () => {
|
|
317
|
+
const data = await apiGet("/generate/multi-animation/presets");
|
|
318
|
+
const presets = (data.presets || []).map((p) => `- ${p.key} (${p.label}): ${p.actions.join(", ")}`);
|
|
319
|
+
const qualities = (data.qualities || []).map((q) => `- ${q.key}: ${q.resolution}, ${q.credits} credits per pass`);
|
|
320
|
+
return {
|
|
321
|
+
content: [
|
|
322
|
+
{
|
|
323
|
+
type: "text",
|
|
324
|
+
text: `Presets:\n${presets.join("\n")}\n\nQuality (picked automatically from the sprite size):\n${qualities.join("\n")}\n\nUp to ${data.maxImages} images per run.`,
|
|
325
|
+
},
|
|
326
|
+
],
|
|
327
|
+
};
|
|
328
|
+
})
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
// ---------------------------------------------------------------------------
|
|
332
|
+
// Tagging
|
|
333
|
+
// ---------------------------------------------------------------------------
|
|
334
|
+
|
|
335
|
+
const tagList = z
|
|
336
|
+
.array(z.string().min(1).max(40))
|
|
337
|
+
.min(1)
|
|
338
|
+
.describe("Tags, e.g. ['player', 'level-1']. Case-insensitive duplicates are merged");
|
|
339
|
+
const tagMode = z
|
|
340
|
+
.enum(["add", "remove", "replace"])
|
|
341
|
+
.default("add")
|
|
342
|
+
.describe("add = keep existing tags and add these, remove = drop these, replace = set exactly these");
|
|
343
|
+
|
|
344
|
+
function applyTags(current, tags, mode) {
|
|
345
|
+
const existing = String(current || "").split(",").map((t) => t.trim()).filter(Boolean);
|
|
346
|
+
const lower = new Set(tags.map((t) => t.toLowerCase()));
|
|
347
|
+
const merged = mode === "replace" ? tags : mode === "remove" ? existing.filter((t) => !lower.has(t.toLowerCase())) : [...existing, ...tags];
|
|
348
|
+
// Same rule as the server's normalizeTags: case-insensitive duplicates keep the first spelling.
|
|
349
|
+
const seen = new Map();
|
|
350
|
+
for (const t of merged) if (!seen.has(t.toLowerCase())) seen.set(t.toLowerCase(), t);
|
|
351
|
+
return [...seen.values()];
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
server.tool(
|
|
355
|
+
"tag_asset",
|
|
356
|
+
"Add, remove or replace the tags on an asset, so it can be found again with list_assets(tag). Free.",
|
|
357
|
+
{ id: assetId, tags: tagList, mode: tagMode },
|
|
358
|
+
tagTool("Tag asset"),
|
|
359
|
+
tool(async ({ id, tags, mode }) => {
|
|
360
|
+
const asset = await apiGet(`/assets/${id}`);
|
|
361
|
+
const next = applyTags(asset.tags, tags, mode).join(",");
|
|
362
|
+
const data = await apiPut(`/assets/${id}`, { data: { tags: next } });
|
|
363
|
+
const saved = (data.data?.attributes || data.data || data).tags ?? next;
|
|
364
|
+
return { content: [{ type: "text", text: `Asset ${id} tags: ${saved || "(none)"}` }] };
|
|
365
|
+
})
|
|
366
|
+
);
|
|
367
|
+
|
|
368
|
+
server.tool(
|
|
369
|
+
"tag_frame",
|
|
370
|
+
"Add, remove or replace the tags on one frame of an image asset (an animation, edit or part), e.g. " +
|
|
371
|
+
"to mark which clip is 'idle' or 'final'. 8-way rotation frames cannot be tagged: their tags are the " +
|
|
372
|
+
"facings the animation presets rely on. Free.",
|
|
373
|
+
{
|
|
374
|
+
id: assetId,
|
|
375
|
+
frameIndex: z.number().int().describe("Frame id from get_asset"),
|
|
376
|
+
tags: tagList,
|
|
377
|
+
mode: tagMode,
|
|
378
|
+
},
|
|
379
|
+
tagTool("Tag frame"),
|
|
380
|
+
tool(async ({ id, frameIndex, tags, mode }) => {
|
|
381
|
+
const asset = await apiGet(`/assets/${id}`);
|
|
382
|
+
const frame = (asset.meta_data?.frames || []).find((f) => f.id === frameIndex && !f.archived);
|
|
383
|
+
if (!frame) throw new Error(`Asset ${id} has no frame ${frameIndex}. Use get_asset ${id} for its frame ids.`);
|
|
384
|
+
if (isRotation(frame)) {
|
|
385
|
+
throw new Error(`Frame ${frameIndex} is an 8-way rotation: its tags are its facings, which animation presets read. Tag the asset instead.`);
|
|
386
|
+
}
|
|
387
|
+
if (!frame.images?.length) throw new Error(`Frame ${frameIndex} has no images to locate it by.`);
|
|
388
|
+
const next = applyTags(frame.tags, tags, mode).join(",");
|
|
389
|
+
// The server finds the frame by one of its image files, which stays stable across reindexing.
|
|
390
|
+
const data = await apiPut(`/assets/${id}/frame-tags`, { referenceFile: frame.images[0], tags: next });
|
|
391
|
+
return { content: [{ type: "text", text: `Frame ${frameIndex} of asset ${id} tags: ${data.tags || "(none)"}` }] };
|
|
392
|
+
})
|
|
393
|
+
);
|
|
157
394
|
|
|
158
395
|
// ---------------------------------------------------------------------------
|
|
159
|
-
//
|
|
160
|
-
// POST /api/generate/image
|
|
396
|
+
// Images
|
|
161
397
|
// ---------------------------------------------------------------------------
|
|
398
|
+
|
|
162
399
|
server.tool(
|
|
163
400
|
"generate_image",
|
|
164
|
-
"Generate a game sprite/asset image. Infer every field from the
|
|
165
|
-
"
|
|
166
|
-
"
|
|
401
|
+
"Generate a game sprite/asset image. Infer every field from the request: art STYLE, colour PALETTE, " +
|
|
402
|
+
"asset TYPE, output SIZE and PERSPECTIVE; for characters also facing/pose/bodyType. Costs by model: " +
|
|
403
|
+
"flux2-klein 1, flux-juggernaut 1, seedream-5-flash 2, gpt-image-2.5 5, retro-diffusion 10, " +
|
|
404
|
+
"nano-banana-2 15 credits (a reference image makes it 5 and uses flux2-klein).",
|
|
167
405
|
{
|
|
168
|
-
prompt: z.string().max(300).describe("
|
|
406
|
+
prompt: z.string().max(300).describe("The subject only (max 300 chars). Put style/colour in the style/palette fields."),
|
|
169
407
|
model: z
|
|
170
|
-
.enum([
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
"
|
|
174
|
-
|
|
175
|
-
|
|
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."),
|
|
408
|
+
.enum(["flux2-klein", "gpt-image-2.5", "seedream-5-flash", "nano-banana-2", "retro-diffusion", "flux-juggernaut"])
|
|
409
|
+
.default("flux2-klein")
|
|
410
|
+
.describe(
|
|
411
|
+
"flux2-klein: fast, cheap default. retro-diffusion: true 8/16-bit pixel art. gpt-image-2.5 / nano-banana-2: " +
|
|
412
|
+
"best prompt following and text. seedream-5-flash / flux-juggernaut: painterly and detailed."
|
|
413
|
+
),
|
|
189
414
|
type: z
|
|
190
|
-
.enum(["object", "character", "character portrait", "artwork", "tileset"])
|
|
191
|
-
.describe(
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
415
|
+
.enum(["object", "character", "character portrait", "artwork", "tileset", "ui"])
|
|
416
|
+
.describe(
|
|
417
|
+
"'character' = full-body sprite (uses facing/pose/bodyType); 'character portrait' = headshot; 'object' = props " +
|
|
418
|
+
"and items; 'artwork' = scenes/backgrounds; 'tileset' = map tiles; 'ui' = interface pieces (see uiMode)."
|
|
419
|
+
),
|
|
420
|
+
dimension: z.enum(["2d", "top down", "isometric"]).default("2d").describe("'2d' = side view, 'top down' = overhead, 'isometric' = 2.5D"),
|
|
195
421
|
size: z
|
|
196
|
-
.union([z.literal(32), z.literal(64), z.literal(128), z.literal(512)])
|
|
197
|
-
.
|
|
198
|
-
|
|
422
|
+
.union([z.literal(32), z.literal(64), z.literal(128), z.literal(512), z.literal(1024)])
|
|
423
|
+
.default(512)
|
|
424
|
+
.describe("Output px. 32/64/128 give a pixel-art look; 512 for every other style; 1024 for high-res."),
|
|
425
|
+
colours: z.number().int().min(4).max(12).default(8).describe("Colour count for pixel-art reduction (4–12)"),
|
|
199
426
|
style: z
|
|
200
427
|
.string()
|
|
201
428
|
.default("")
|
|
202
429
|
.describe(
|
|
203
|
-
"
|
|
204
|
-
"
|
|
205
|
-
"Leave empty to auto-pick from size. Pixel-art style requires size 32/64/128."
|
|
430
|
+
"Style preset: pixel-art, cartoon, cel-shading, flat, hand-drawn, paper-cutout, comic-book, anime, watercolor, " +
|
|
431
|
+
"low-poly, 3d-render, claymation, photorealistic, voxel. Empty = pixel art below 512, cartoon otherwise."
|
|
206
432
|
),
|
|
207
433
|
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
|
-
])
|
|
434
|
+
.enum(["none", "black-and-white", "retro", "vibrant", "pastel", "neon", "earthy", "gameboy", "sepia"])
|
|
219
435
|
.default("none")
|
|
220
|
-
.describe("Colour-
|
|
221
|
-
facing: z.enum(["forward", "right"]).default("forward").describe("
|
|
222
|
-
pose: z.enum(["idle", "tpose"]).default("idle").describe("
|
|
223
|
-
bodyType: z.enum(["humanoid", "quadruped"]).default("humanoid").describe("
|
|
436
|
+
.describe("Colour-scheme preset, independent of style. 'none' keeps the style's own colours."),
|
|
437
|
+
facing: z.enum(["forward", "right"]).default("forward").describe("Characters: front view or side profile. Side profiles rotate best."),
|
|
438
|
+
pose: z.enum(["idle", "tpose"]).default("idle").describe("Characters: relaxed stance, or T-pose for rigging"),
|
|
439
|
+
bodyType: z.enum(["humanoid", "quadruped"]).default("humanoid").describe("Characters: body plan"),
|
|
440
|
+
uiMode: z
|
|
441
|
+
.enum(["graphic", "title", "text"])
|
|
442
|
+
.default("graphic")
|
|
443
|
+
.describe("type 'ui' only: 'graphic' = button/panel/HUD piece, 'title' = game-title wordmark spelling the prompt, 'text' = plain label spelling the prompt"),
|
|
224
444
|
seed: z.number().int().min(0).default(0).describe("RNG seed (0 = random)"),
|
|
225
|
-
referenceImage: z.string().default("").describe("
|
|
226
|
-
styleImage: z.string().default("").describe("
|
|
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)."),
|
|
445
|
+
referenceImage: z.string().default("").describe("Optional raw base64 image (no data: prefix) to base the result on"),
|
|
446
|
+
styleImage: z.string().default("").describe("Optional raw base64 image whose art style to copy"),
|
|
229
447
|
},
|
|
230
448
|
writeTool("Generate image"),
|
|
231
|
-
async (
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
-
}
|
|
449
|
+
tool(async ({ facing, pose, bodyType, palette, uiMode, ...rest }) => {
|
|
450
|
+
const body = { ...rest, palette: [], options: { facing, pose, bodyType, palette, mode: uiMode } };
|
|
451
|
+
return assetResult(await apiPost("/generate/image", body), "Image");
|
|
452
|
+
})
|
|
243
453
|
);
|
|
244
454
|
|
|
245
|
-
|
|
246
|
-
// ---------------------------------------------------------------------------
|
|
247
|
-
// Image edit (inpainting / outpainting style edit)
|
|
248
|
-
// POST /api/generate/image-edit
|
|
249
|
-
// ---------------------------------------------------------------------------
|
|
250
455
|
server.tool(
|
|
251
|
-
"
|
|
252
|
-
"Edit an existing image with a
|
|
456
|
+
"edit_image",
|
|
457
|
+
"Edit an existing image asset with an instruction (change colours, add a hat, fix a detail…). The edit is " +
|
|
458
|
+
"added as a new frame; the original stays. 5 credits.",
|
|
253
459
|
{
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
model: z
|
|
460
|
+
id: assetId,
|
|
461
|
+
prompt: z.string().describe("What to change"),
|
|
462
|
+
model: z
|
|
463
|
+
.enum(["p-image", "klein", "nano", "gpt-image-2.5"])
|
|
464
|
+
.default("p-image")
|
|
465
|
+
.describe("p-image: the app's default editor. nano: most faithful to the original. gpt-image-2.5: best at following complex changes. klein: fastest."),
|
|
466
|
+
referenceFile,
|
|
467
|
+
frameId,
|
|
468
|
+
images: z.array(z.string()).max(4).default([]).describe("Optional extra reference images: image keys from get_asset or https URLs"),
|
|
469
|
+
remove_bg: z.boolean().default(false).describe("Remove the background from the result"),
|
|
257
470
|
},
|
|
258
471
|
writeTool("Edit image"),
|
|
259
|
-
async (args) =>
|
|
472
|
+
tool(async (args) => framesResult(await apiPost("/generate/image-edit", args), args.id, "Edit"))
|
|
260
473
|
);
|
|
261
474
|
|
|
475
|
+
server.tool(
|
|
476
|
+
"rotate_sprite",
|
|
477
|
+
"Turn a sprite into an 8-way set (front, back, sides and diagonals) in one pass. The result is a " +
|
|
478
|
+
"'rotations - 8-way' frame whose facings line up with its images; pass those to generate_animation_set " +
|
|
479
|
+
"to animate each direction. Works best from a side-profile sprite. 10 credits.",
|
|
480
|
+
{ id: assetId, referenceFile, frameId },
|
|
481
|
+
writeTool("Rotate sprite (8-way)"),
|
|
482
|
+
tool(async (args) => framesResult(await apiPost("/generate/image-rotations", args), args.id, "8-way rotation"))
|
|
483
|
+
);
|
|
262
484
|
|
|
263
|
-
// ---------------------------------------------------------------------------
|
|
264
|
-
// Image edit — rotate (multi-angle sprite sheet)
|
|
265
|
-
// POST /api/generate/image-rotations
|
|
266
|
-
// ---------------------------------------------------------------------------
|
|
267
485
|
server.tool(
|
|
268
|
-
"
|
|
269
|
-
"
|
|
486
|
+
"extract_parts",
|
|
487
|
+
"Split a character sprite into separate body parts (head, torso, arms, legs…) as 'layer' frames, for " +
|
|
488
|
+
"rigging or cut-out animation. 5 credits. Call get_asset afterwards for the parts' frame ids.",
|
|
270
489
|
{
|
|
271
|
-
id:
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
referenceFile: z.string().optional().describe("Frame filename to use as the base (optional)"),
|
|
490
|
+
id: assetId,
|
|
491
|
+
prompt: z.string().default("").describe("Optional: which parts to cut out. Empty = the standard rig parts"),
|
|
492
|
+
referenceFile,
|
|
493
|
+
frameId,
|
|
276
494
|
},
|
|
277
|
-
writeTool("
|
|
278
|
-
async (args) =>
|
|
495
|
+
writeTool("Extract sprite parts"),
|
|
496
|
+
tool(async (args) => {
|
|
497
|
+
const data = await apiPost("/generate/image-parts", args);
|
|
498
|
+
// The ids returned here are off by (parts - 1): the server numbers from the last appended
|
|
499
|
+
// frame. Point at get_asset instead of printing ids that would misdirect a follow-up call.
|
|
500
|
+
const names = (data.frames || []).map((f) => f.name).filter(Boolean);
|
|
501
|
+
return {
|
|
502
|
+
content: [
|
|
503
|
+
{
|
|
504
|
+
type: "text",
|
|
505
|
+
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.`,
|
|
506
|
+
},
|
|
507
|
+
],
|
|
508
|
+
};
|
|
509
|
+
})
|
|
279
510
|
);
|
|
280
511
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
// ---------------------------------------------------------------------------
|
|
284
|
-
// Image parts (isolate / extract sprite parts)
|
|
285
|
-
// POST /api/generate/image-parts
|
|
286
|
-
// ---------------------------------------------------------------------------
|
|
287
512
|
server.tool(
|
|
288
|
-
"
|
|
289
|
-
"
|
|
513
|
+
"upload_image",
|
|
514
|
+
"Add an image the user already has to their library, so it can be animated, rotated or edited. It must " +
|
|
515
|
+
"be square at 32, 64, 128, 512 or 1024 px. Free.",
|
|
290
516
|
{
|
|
291
|
-
|
|
292
|
-
|
|
517
|
+
image: z.string().describe("Base64 image (a data: prefix is stripped automatically)"),
|
|
518
|
+
type: z.enum(["object", "character", "character portrait", "artwork"]).default("character"),
|
|
519
|
+
remove_bg: z.boolean().default(false).describe("Remove the background on upload"),
|
|
293
520
|
},
|
|
294
|
-
writeTool("
|
|
295
|
-
async (
|
|
521
|
+
writeTool("Upload image"),
|
|
522
|
+
tool(async ({ image, ...rest }) => {
|
|
523
|
+
const raw = String(image).replace(/^data:[^,]*,/, "");
|
|
524
|
+
return assetResult(await apiPost("/upload/image", { image: raw, ...rest }), "Upload");
|
|
525
|
+
})
|
|
296
526
|
);
|
|
297
527
|
|
|
298
|
-
// ---------------------------------------------------------------------------
|
|
299
|
-
// Image animation
|
|
300
|
-
// POST /api/generate/image-animation
|
|
301
|
-
// ---------------------------------------------------------------------------
|
|
302
528
|
server.tool(
|
|
303
|
-
"
|
|
304
|
-
"
|
|
529
|
+
"pixelate_image",
|
|
530
|
+
"Convert any image to pixel art at a target size and colour count. Returns the image; nothing is " +
|
|
531
|
+
"saved to the library. 1 credit.",
|
|
305
532
|
{
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
end_frame: z.string().default("").describe("Base64 or filename for the last frame (optional)"),
|
|
533
|
+
image: z.string().describe("Base64 source image (a data: prefix is stripped automatically)"),
|
|
534
|
+
size: z.union([z.literal(32), z.literal(64), z.literal(128), z.literal(512)]).default(64),
|
|
535
|
+
colours: z.number().int().min(4).max(12).default(8),
|
|
310
536
|
},
|
|
311
|
-
writeTool("
|
|
312
|
-
async (
|
|
537
|
+
writeTool("Pixelate image"),
|
|
538
|
+
tool(async ({ image, ...rest }) => {
|
|
539
|
+
const data = await apiPost("/pixelate/image", { image: String(image).replace(/^data:[^,]*,/, ""), ...rest });
|
|
540
|
+
return {
|
|
541
|
+
content: [
|
|
542
|
+
{ type: "text", text: money(data) || "Pixelated." },
|
|
543
|
+
{ type: "image", data: data.image, mimeType: "image/png" },
|
|
544
|
+
],
|
|
545
|
+
};
|
|
546
|
+
})
|
|
313
547
|
);
|
|
314
548
|
|
|
315
|
-
|
|
316
549
|
// ---------------------------------------------------------------------------
|
|
317
|
-
//
|
|
318
|
-
// POST /api/generate/multi-animation
|
|
550
|
+
// Animation
|
|
319
551
|
// ---------------------------------------------------------------------------
|
|
552
|
+
|
|
320
553
|
server.tool(
|
|
321
|
-
"
|
|
322
|
-
"
|
|
323
|
-
"
|
|
324
|
-
"generate_image_animation calls when more than one clip is wanted: it is a single billed job.",
|
|
554
|
+
"animate_sprite",
|
|
555
|
+
"Animate one sprite into a single 16-frame clip from a description of the motion. For several clips " +
|
|
556
|
+
"(walk + run + attack, or every direction) use generate_animation_set instead: it is one job. 15 credits.",
|
|
325
557
|
{
|
|
326
|
-
|
|
327
|
-
prompt: z.string().describe("
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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"),
|
|
558
|
+
id: assetId,
|
|
559
|
+
prompt: z.string().describe("The motion, e.g. 'walk cycle', 'swings sword overhead'"),
|
|
560
|
+
referenceFile,
|
|
561
|
+
end_frame: z.string().default("").describe("Optional image key for the clip to end on (e.g. to loop back to a pose). Empty = free ending"),
|
|
562
|
+
frameId,
|
|
342
563
|
},
|
|
343
|
-
writeTool("
|
|
344
|
-
async (args) =>
|
|
564
|
+
writeTool("Animate sprite"),
|
|
565
|
+
tool(async (args) => framesResult(await apiPost("/generate/image-animation", { ...args, mode: "f" }), args.id, "Animation"))
|
|
345
566
|
);
|
|
346
567
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
568
|
+
const pick = z.object({
|
|
569
|
+
assetId: z.number().int().describe("Asset to animate"),
|
|
570
|
+
frameIndex: z.number().int().nullable().optional().describe("Frame id from get_asset (e.g. an 8-way rotation frame). Omit for the asset's base image"),
|
|
571
|
+
imageIndex: z.number().int().optional().describe("Which image of that frame (for a rotation frame: the facing's position in its facings list)"),
|
|
572
|
+
base: z.boolean().optional().describe("true = the asset's original base image, which counts as the 'down' (front) facing"),
|
|
573
|
+
});
|
|
574
|
+
|
|
351
575
|
server.tool(
|
|
352
|
-
"
|
|
353
|
-
"
|
|
576
|
+
"generate_animation_set",
|
|
577
|
+
"Generate several animations in one job. Two ways to use it:\n" +
|
|
578
|
+
"1. A PRESET on one sprite: platformer-character (idle, run, jump, attack-1, death, hurt) or a top-down " +
|
|
579
|
+
"set (topdown-walk/run/dash/attack: 8 directions; topdown-idle: 4). Each item gets the preset's actions.\n" +
|
|
580
|
+
"2. A PRESET across an 8-way rotation set: one item per facing — {assetId, base:true} for 'down' plus " +
|
|
581
|
+
"{assetId, frameIndex:<rotation frame id>, imageIndex:i} for each other facing — and every facing gets its " +
|
|
582
|
+
"own directional clip (walk-left from the left-facing image, …). For topdown-idle pick only the 4 " +
|
|
583
|
+
"cardinals (down, right, up, left).\n" +
|
|
584
|
+
"Without a preset, describe the actions in the prompt ('idle, run, jump') for up to 6 custom clips. " +
|
|
585
|
+
"Cost: 100 credits per pass (200 for 1024px sprites, which run in 4K); a pass holds up to 6 clips, so an " +
|
|
586
|
+
"8-way set is 2 passes. Clips are added to their source asset as frames named after the action.",
|
|
354
587
|
{
|
|
355
|
-
items: z.array(
|
|
356
|
-
|
|
357
|
-
|
|
588
|
+
items: z.array(pick).min(1).max(12).describe("Images to animate, in order"),
|
|
589
|
+
preset: z
|
|
590
|
+
.enum(["platformer-character", "topdown-walk", "topdown-run", "topdown-dash", "topdown-attack", "topdown-idle"])
|
|
591
|
+
.optional()
|
|
592
|
+
.describe("Named set of actions (see get_animation_presets)"),
|
|
593
|
+
prompt: z.string().default("").describe("Without a preset: the actions to make. With one: optional extra direction for every clip"),
|
|
594
|
+
holdFacing: z.boolean().default(false).describe("End every clip on the frame it started from, so directional loops do not drift"),
|
|
358
595
|
},
|
|
359
|
-
writeTool("
|
|
360
|
-
async (args) =>
|
|
596
|
+
writeTool("Generate animation set"),
|
|
597
|
+
tool(async (args) => resultsContent(await apiPost("/generate/multi-animation", args), "Animation set generated."))
|
|
361
598
|
);
|
|
362
599
|
|
|
363
|
-
// ---------------------------------------------------------------------------
|
|
364
|
-
// Image animation edit
|
|
365
|
-
// POST /api/generate/image-animation-edit
|
|
366
|
-
// ---------------------------------------------------------------------------
|
|
367
600
|
server.tool(
|
|
368
|
-
"
|
|
369
|
-
"
|
|
601
|
+
"edit_animation_set",
|
|
602
|
+
"Re-generate existing animations with a change (e.g. 'make the attack faster', 'add a cape that " +
|
|
603
|
+
"flows'), as new clips next to the originals. This re-animates; to change how the character LOOKS " +
|
|
604
|
+
"while keeping the exact motion, use preview_keep_motion_edit. 100 credits (200 at 1024px) per run.",
|
|
370
605
|
{
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
606
|
+
items: z
|
|
607
|
+
.array(z.object({ assetId: z.number().int(), frameIndex: z.number().int().describe("Animation frame id from get_asset") }))
|
|
608
|
+
.min(1)
|
|
609
|
+
.max(12),
|
|
610
|
+
prompt: z.string().describe("The change to make"),
|
|
374
611
|
},
|
|
375
|
-
writeTool("Edit animation
|
|
376
|
-
async (args) =>
|
|
612
|
+
writeTool("Edit animation set"),
|
|
613
|
+
tool(async (args) => resultsContent(await apiPost("/generate/multi-animation/edit", args), "Animations edited."))
|
|
377
614
|
);
|
|
378
615
|
|
|
616
|
+
// Keep motion: the preview has to be approved before it is applied, and the apply call must send
|
|
617
|
+
// the preview's files back unchanged. They are held here per asset so the model only has to say
|
|
618
|
+
// "apply" with the same prompt; explicit preview/stages still win if given.
|
|
619
|
+
const pendingKeepMotion = new Map();
|
|
379
620
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
// ---------------------------------------------------------------------------
|
|
383
|
-
// Image pixelation
|
|
384
|
-
// POST /api/pixelate/image
|
|
385
|
-
// ---------------------------------------------------------------------------
|
|
386
621
|
server.tool(
|
|
387
|
-
"
|
|
388
|
-
"
|
|
622
|
+
"preview_keep_motion_edit",
|
|
623
|
+
"Step 1 of a 'keep motion' edit: change how a character looks (outfit, colours, a held item…) across " +
|
|
624
|
+
"existing animations WITHOUT re-animating them. This draws the change on one frame for approval and saves " +
|
|
625
|
+
"nothing. Show it to the user; if they approve, call apply_keep_motion_edit with the same id, prompt and " +
|
|
626
|
+
"animations. 5 credits (10 if the prompt both changes the character and adds an item).",
|
|
389
627
|
{
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
628
|
+
id: assetId,
|
|
629
|
+
prompt: z.string().describe("The look change, e.g. 'red armour and a blue cape'"),
|
|
630
|
+
animations: z.array(z.number().int()).default([]).describe("Animation frame ids to edit (from get_asset). The preview uses the first one"),
|
|
393
631
|
},
|
|
394
|
-
writeTool("
|
|
395
|
-
async (
|
|
632
|
+
writeTool("Preview keep-motion edit"),
|
|
633
|
+
tool(async ({ id, prompt, animations }) => {
|
|
634
|
+
const data = await apiPost("/generate/frame-edit/preview", { id, prompt, items: animations });
|
|
635
|
+
pendingKeepMotion.set(id, { prompt, preview: data.preview, stages: data.stages });
|
|
636
|
+
const content = [
|
|
637
|
+
{
|
|
638
|
+
type: "text",
|
|
639
|
+
text: [
|
|
640
|
+
`Preview of "${prompt}" on asset ${id}. Nothing is saved yet.`,
|
|
641
|
+
data.stages?.added ? `Split into: change "${data.stages.whole}" + add "${data.stages.added}".` : null,
|
|
642
|
+
money(data),
|
|
643
|
+
`If the user approves, call apply_keep_motion_edit with id ${id}, the same prompt and the animations to change.`,
|
|
644
|
+
]
|
|
645
|
+
.filter(Boolean)
|
|
646
|
+
.join("\n"),
|
|
647
|
+
},
|
|
648
|
+
];
|
|
649
|
+
const image = await inlineImage(data.preview?.images?.[0]);
|
|
650
|
+
if (image) content.push(image);
|
|
651
|
+
return { content };
|
|
652
|
+
})
|
|
396
653
|
);
|
|
397
654
|
|
|
398
|
-
// ---------------------------------------------------------------------------
|
|
399
|
-
// Image upload
|
|
400
|
-
// POST /api/upload/image
|
|
401
|
-
// ---------------------------------------------------------------------------
|
|
402
655
|
server.tool(
|
|
403
|
-
"
|
|
404
|
-
"
|
|
656
|
+
"apply_keep_motion_edit",
|
|
657
|
+
"Step 2 of a 'keep motion' edit: apply an approved preview to every frame of the chosen animations, " +
|
|
658
|
+
"keeping their motion exactly. New animations named '<original> (<change>)' are added next to the " +
|
|
659
|
+
"originals. Costs 15 credits per 16 frames per pass (1 pass, or 2 when the change also adds an item); " +
|
|
660
|
+
"at most 256 frames.",
|
|
405
661
|
{
|
|
406
|
-
|
|
407
|
-
|
|
662
|
+
id: assetId,
|
|
663
|
+
prompt: z.string().describe("The same prompt as the approved preview"),
|
|
664
|
+
animations: z.array(z.number().int()).min(1).describe("Animation frame ids to apply it to"),
|
|
665
|
+
frameId,
|
|
408
666
|
},
|
|
409
|
-
writeTool("
|
|
410
|
-
async (
|
|
667
|
+
writeTool("Apply keep-motion edit"),
|
|
668
|
+
tool(async ({ id, prompt, animations, frameId }) => {
|
|
669
|
+
const pending = pendingKeepMotion.get(id);
|
|
670
|
+
if (!pending || pending.prompt !== prompt) {
|
|
671
|
+
throw new Error(`No approved preview for asset ${id} with that prompt. Call preview_keep_motion_edit first.`);
|
|
672
|
+
}
|
|
673
|
+
const data = await apiPost("/generate/frame-edit", {
|
|
674
|
+
id,
|
|
675
|
+
prompt,
|
|
676
|
+
items: animations,
|
|
677
|
+
frameId,
|
|
678
|
+
preview: pending.preview,
|
|
679
|
+
stages: pending.stages,
|
|
680
|
+
});
|
|
681
|
+
pendingKeepMotion.delete(id);
|
|
682
|
+
return framesResult(data, id, "Keep-motion edit");
|
|
683
|
+
})
|
|
411
684
|
);
|
|
412
685
|
|
|
413
686
|
// ---------------------------------------------------------------------------
|
|
414
|
-
//
|
|
415
|
-
// POST /api/generate/sfx
|
|
687
|
+
// Audio
|
|
416
688
|
// ---------------------------------------------------------------------------
|
|
689
|
+
|
|
417
690
|
server.tool(
|
|
418
691
|
"generate_sfx",
|
|
419
|
-
"Generate a short sound effect
|
|
420
|
-
{
|
|
421
|
-
prompt: z.string().describe("Description of the sound effect to generate"),
|
|
422
|
-
},
|
|
692
|
+
"Generate a short sound effect (coin pickup, sword hit, footstep…). 5 credits.",
|
|
693
|
+
{ prompt: z.string().describe("The sound to make") },
|
|
423
694
|
writeTool("Generate sound effect"),
|
|
424
|
-
async (
|
|
695
|
+
tool(async ({ prompt }) => assetResult(await apiPost("/generate/sfx", { prompt }), "Sound effect"))
|
|
425
696
|
);
|
|
426
697
|
|
|
427
|
-
// ---------------------------------------------------------------------------
|
|
428
|
-
// Music generation
|
|
429
|
-
// POST /api/generate/music
|
|
430
|
-
// ---------------------------------------------------------------------------
|
|
431
698
|
server.tool(
|
|
432
699
|
"generate_music",
|
|
433
|
-
"Generate a music track
|
|
700
|
+
"Generate a music track. 10 credits.",
|
|
434
701
|
{
|
|
435
|
-
prompt: z.string().describe("
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
.describe("small = short track (10 credits), medium = longer track (20 credits)"),
|
|
702
|
+
prompt: z.string().min(10).max(300).describe("Genre, mood and instruments, e.g. 'upbeat chiptune for a title screen' (10–300 chars)"),
|
|
703
|
+
duration: z.number().int().min(30).max(120).default(60).describe("Seconds (30–120)"),
|
|
704
|
+
bpm: z.number().int().min(80).max(200).default(100),
|
|
705
|
+
keyScale: z.string().default("C major").describe("e.g. 'A minor', 'D major'"),
|
|
440
706
|
},
|
|
441
707
|
writeTool("Generate music"),
|
|
442
|
-
async (args) =>
|
|
708
|
+
tool(async (args) => assetResult(await apiPost("/generate/music", args), "Music"))
|
|
443
709
|
);
|
|
444
710
|
|
|
445
711
|
// ---------------------------------------------------------------------------
|
|
446
|
-
// Video
|
|
447
|
-
// POST /api/generate/video
|
|
712
|
+
// Video
|
|
448
713
|
// ---------------------------------------------------------------------------
|
|
714
|
+
|
|
449
715
|
server.tool(
|
|
450
716
|
"generate_video",
|
|
451
|
-
"Generate a short video clip from a
|
|
717
|
+
"Generate a short video clip (trailer shot, cutscene, animated background) from a prompt and 1–7 " +
|
|
718
|
+
"reference images. 15 credits per second.",
|
|
452
719
|
{
|
|
453
|
-
prompt: z.string().describe("
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
palette: z.array(z.string()).default([]).describe("Colour palette (optional)"),
|
|
720
|
+
prompt: z.string().describe("What happens in the clip"),
|
|
721
|
+
images: z.array(z.string()).min(1).max(7).describe("Reference images: image keys from get_asset, https URLs, or data: URIs"),
|
|
722
|
+
duration: z.number().int().min(4).max(15).default(4).describe("Seconds (4–15)"),
|
|
723
|
+
width: z.number().int().default(1280),
|
|
724
|
+
height: z.number().int().default(720),
|
|
459
725
|
},
|
|
460
726
|
writeTool("Generate video"),
|
|
461
|
-
async (args) =>
|
|
727
|
+
tool(async (args) => assetResult(await apiPost("/generate/video", args), "Video"))
|
|
462
728
|
);
|
|
463
729
|
|
|
464
|
-
// ---------------------------------------------------------------------------
|
|
465
|
-
// Video extend
|
|
466
|
-
// POST /api/generate/video-extend
|
|
467
|
-
// ---------------------------------------------------------------------------
|
|
468
730
|
server.tool(
|
|
469
|
-
"
|
|
470
|
-
"
|
|
731
|
+
"extend_video",
|
|
732
|
+
"Continue an existing video asset from its last frame. 15 credits per second.",
|
|
471
733
|
{
|
|
472
|
-
assetId: z.number().int().describe("
|
|
473
|
-
prompt: z.string().describe("
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
palette: z.array(z.string()).default([]).describe("Colour palette (optional)"),
|
|
734
|
+
assetId: z.number().int().describe("The video asset"),
|
|
735
|
+
prompt: z.string().describe("What happens next"),
|
|
736
|
+
video: z.string().default("").describe("Which clip to continue (a file key from get_asset). Empty = the latest extension, else the original"),
|
|
737
|
+
duration: z.number().int().min(4).max(15).default(4),
|
|
738
|
+
width: z.number().int().default(1280),
|
|
739
|
+
height: z.number().int().default(720),
|
|
479
740
|
},
|
|
480
741
|
writeTool("Extend video"),
|
|
481
|
-
async (
|
|
742
|
+
tool(async ({ video, ...args }) => {
|
|
743
|
+
let source = video;
|
|
744
|
+
if (!source) {
|
|
745
|
+
const asset = await apiGet(`/assets/${args.assetId}`);
|
|
746
|
+
const extensions = asset.meta_data?.video_extensions || [];
|
|
747
|
+
source = extensions.length ? extensions[extensions.length - 1].file : asset.file;
|
|
748
|
+
}
|
|
749
|
+
const data = await apiPost("/generate/video-extend", { ...args, video: source, images: [] });
|
|
750
|
+
const clips = (data.videos || []).map((v) => `- ${cdnUrl(v.file)}`).join("\n");
|
|
751
|
+
return { content: [{ type: "text", text: `Extended video ${args.assetId}:\n${clips}\n${money(data)}` }] };
|
|
752
|
+
})
|
|
482
753
|
);
|
|
483
754
|
|
|
484
755
|
// ---------------------------------------------------------------------------
|
|
485
|
-
// 3D
|
|
486
|
-
// POST /api/generate/3d-model
|
|
756
|
+
// 3D
|
|
487
757
|
// ---------------------------------------------------------------------------
|
|
758
|
+
|
|
488
759
|
server.tool(
|
|
489
760
|
"generate_3d_model",
|
|
490
|
-
"Generate a 3D model from a
|
|
761
|
+
"Generate a 3D model (GLB) from a prompt. 15 credits (lq) or 25 (hq).",
|
|
491
762
|
{
|
|
492
|
-
prompt: z.string().describe("
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
763
|
+
prompt: z.string().describe("The model to make"),
|
|
764
|
+
type: z.enum(["object", "character"]).default("object").describe("'character' makes a T-posed figure ready for rigging"),
|
|
765
|
+
style: z.string().default("cartoon style").describe("e.g. 'cartoon style', 'low-poly', 'realistic'"),
|
|
766
|
+
mode: z.enum(["lq", "hq"]).default("hq").describe("lq = fast draft, hq = detailed"),
|
|
767
|
+
bodyType: z.enum(["humanoid", "quadruped"]).default("humanoid").describe("Characters only"),
|
|
768
|
+
seed: z.number().int().min(0).default(0),
|
|
498
769
|
},
|
|
499
770
|
writeTool("Generate 3D model"),
|
|
500
|
-
async (
|
|
771
|
+
tool(async ({ bodyType, ...rest }) => {
|
|
772
|
+
const options = rest.type === "character" ? { pose: "tpose", bodyType } : {};
|
|
773
|
+
return assetResult(await apiPost("/generate/3d-model", { ...rest, options }), "3D model");
|
|
774
|
+
})
|
|
501
775
|
);
|
|
502
776
|
|
|
503
|
-
// ---------------------------------------------------------------------------
|
|
504
|
-
// 3D model rigging
|
|
505
|
-
// POST /api/generate/model-rig
|
|
506
|
-
// ---------------------------------------------------------------------------
|
|
507
777
|
server.tool(
|
|
508
|
-
"
|
|
509
|
-
"
|
|
510
|
-
"
|
|
778
|
+
"rig_3d_model",
|
|
779
|
+
"Add a skeleton to a humanoid 3D model so it can be animated (non-humanoids are refused before any " +
|
|
780
|
+
"charge). Run before animate_3d_model. 140 credits.",
|
|
511
781
|
{
|
|
512
|
-
id: z.number().int().describe("
|
|
782
|
+
id: z.number().int().describe("3D model asset ID"),
|
|
783
|
+
height_meters: z.number().min(0.2).max(10).default(1.7).describe("Character height in metres"),
|
|
513
784
|
},
|
|
514
785
|
writeTool("Rig 3D model"),
|
|
515
|
-
async (args) =>
|
|
786
|
+
tool(async (args) => {
|
|
787
|
+
const data = await apiPost("/generate/model-rig", args);
|
|
788
|
+
return { content: [{ type: "text", text: `Rigged model ${args.id}: ${cdnUrl(data.rigged_glb || data.rigged_file)}\n${money(data)}` }] };
|
|
789
|
+
})
|
|
516
790
|
);
|
|
517
791
|
|
|
518
|
-
// ---------------------------------------------------------------------------
|
|
519
|
-
// 3D model animation
|
|
520
|
-
// POST /api/generate/model-animation
|
|
521
|
-
// ---------------------------------------------------------------------------
|
|
522
792
|
server.tool(
|
|
523
|
-
"
|
|
524
|
-
"Animate a rigged 3D model from a
|
|
525
|
-
"(generate_3d_model_rig) — an unrigged model has no skeleton to drive.",
|
|
793
|
+
"animate_3d_model",
|
|
794
|
+
"Animate a rigged 3D model from a description of the motion. 30 credits.",
|
|
526
795
|
{
|
|
527
|
-
id: z.number().int().describe("
|
|
528
|
-
prompt: z.string().describe("The motion
|
|
796
|
+
id: z.number().int().describe("Rigged 3D model asset ID"),
|
|
797
|
+
prompt: z.string().describe("The motion, e.g. 'walk cycle', 'idle breathing'"),
|
|
798
|
+
duration: z.number().min(1).max(10).default(3).describe("Seconds"),
|
|
529
799
|
},
|
|
530
800
|
writeTool("Animate 3D model"),
|
|
531
|
-
async (args) =>
|
|
801
|
+
tool(async (args) => {
|
|
802
|
+
const data = await apiPost("/generate/model-animation", args);
|
|
803
|
+
const anim = data.animation || {};
|
|
804
|
+
return {
|
|
805
|
+
content: [{ type: "text", text: `Animation "${anim.name || args.prompt}" on model ${args.id}: ${cdnUrl(anim.file)}\n${money(data)}` }],
|
|
806
|
+
};
|
|
807
|
+
})
|
|
532
808
|
);
|
|
533
809
|
|
|
534
|
-
|
|
535
810
|
// ---------------------------------------------------------------------------
|
|
536
811
|
// Start
|
|
537
812
|
// ---------------------------------------------------------------------------
|
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.1.0",
|
|
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>",
|