voidless-mcp 1.1.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 +28 -7
- package/gallery.html +176 -0
- package/index.js +7 -798
- package/package.json +7 -3
- package/tools.js +863 -0
package/README.md
CHANGED
|
@@ -1,16 +1,38 @@
|
|
|
1
1
|
# Voidless MCP server
|
|
2
2
|
|
|
3
|
-
Generate game assets from any MCP client (Claude Desktop, Claude Code, Cursor, VS Code, Windsurf and others): sprites, animations, tilesets, sound effects, music, video and 3D models, saved straight to your [Voidless](https://voidless.dev) library.
|
|
3
|
+
Generate game assets from any MCP client (claude.ai, ChatGPT, Claude Desktop, Claude Code, Cursor, VS Code, Windsurf and others): sprites, animations, tilesets, sound effects, music, video and 3D models, saved straight to your [Voidless](https://voidless.dev) library and shown right in the chat.
|
|
4
|
+
|
|
5
|
+
Two ways to connect: the **hosted connector** (paste a URL, sign in, nothing to install) or this **npm package** (runs locally with an API key). Setup guide: [voidless.dev/mcp](https://voidless.dev/mcp).
|
|
4
6
|
|
|
5
7
|
## Requirements
|
|
6
8
|
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
+
- A Voidless account on **Voidless+**
|
|
10
|
+
- For the npm package: Node.js 18 or newer and an API key (starts with `void_`), created at [voidless.dev/account](https://voidless.dev/account)
|
|
9
11
|
|
|
10
12
|
Every generation spends credits from your Voidless account. Tool descriptions list the cost tiers where they differ.
|
|
11
13
|
|
|
12
14
|
## Setup
|
|
13
15
|
|
|
16
|
+
### Hosted connector (no install)
|
|
17
|
+
|
|
18
|
+
Connector URL:
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
https://mcp.voidless.dev/mcp
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
- **Claude** (claude.ai and the Claude apps): **Settings → Connectors → Add custom connector**, name it Voidless and paste the URL.
|
|
25
|
+
- **ChatGPT** (web, paid plans): **Settings → Apps & Connectors → Advanced settings**, turn on **Developer mode**, then **Create**, paste the URL and choose **OAuth**.
|
|
26
|
+
- Any other MCP client that takes a remote server URL works the same way.
|
|
27
|
+
|
|
28
|
+
You'll be asked to sign in to Voidless and click **Allow**; no API key to copy. Results show inline in
|
|
29
|
+
hosts that support MCP Apps: images, every facing of a rotation, animations playing, and audio and video
|
|
30
|
+
players. To disconnect, revoke the "… connector" key under API keys on [voidless.dev/account](https://voidless.dev/account).
|
|
31
|
+
|
|
32
|
+
### Local server (npx)
|
|
33
|
+
|
|
34
|
+
For clients that run a local server, or to work with image files on your own disk.
|
|
35
|
+
|
|
14
36
|
Claude Desktop / Cursor / any client with a JSON config:
|
|
15
37
|
|
|
16
38
|
```json
|
|
@@ -64,9 +86,8 @@ claude mcp add voidless -e VOIDLESS_API_KEY=void_... -- npx -y voidless-mcp
|
|
|
64
86
|
| Tool | What it does |
|
|
65
87
|
| --- | --- |
|
|
66
88
|
| `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
|
-
| `
|
|
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 |
|
|
89
|
+
| `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; the left side is mirrored from the right (`autoFlip` to mirror any right-only clips after the run) |
|
|
90
|
+
| `flip_left_side` | Mirror the right-side animations into the left side (walk-right -> walk-left), free |
|
|
70
91
|
|
|
71
92
|
**Sound, video and 3D**
|
|
72
93
|
|
|
@@ -81,7 +102,7 @@ Generation tools create new content in your library; none deletes or overwrites
|
|
|
81
102
|
## Example prompts
|
|
82
103
|
|
|
83
104
|
- "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
|
-
- "
|
|
105
|
+
- "Mirror my knight's right-facing animations into the left side."
|
|
85
106
|
- "Generate a top-down grass and dirt tileset in a gameboy palette."
|
|
86
107
|
- "Give me a coin pickup sound effect and a short chiptune loop for the title screen."
|
|
87
108
|
- "Create a low-poly treasure chest 3D model, rig it and animate it opening."
|
package/gallery.html
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>Voidless results</title>
|
|
7
|
+
<style>
|
|
8
|
+
/* The inline view an MCP host draws under a Voidless tool call: what the tool made, shown. */
|
|
9
|
+
:root {
|
|
10
|
+
--bg: transparent; --fg: #1d1b20; --muted: #6b6672; --tile: #ecebef; --line: #dcdae0; --accent: #5b5bd6;
|
|
11
|
+
--check: #e3e1e7;
|
|
12
|
+
}
|
|
13
|
+
:root[data-theme="dark"] { --fg: #ecebef; --muted: #a29fa8; --tile: #26252b; --line: #34323a; --accent: #9b9bf0; --check: #2e2d34; }
|
|
14
|
+
@media (prefers-color-scheme: dark) {
|
|
15
|
+
:root:not([data-theme="light"]) { --fg: #ecebef; --muted: #a29fa8; --tile: #26252b; --line: #34323a; --accent: #9b9bf0; --check: #2e2d34; }
|
|
16
|
+
}
|
|
17
|
+
* { box-sizing: border-box; }
|
|
18
|
+
html, body { margin: 0; background: var(--bg); color: var(--fg); font: 14px/1.4 system-ui, -apple-system, "Segoe UI", sans-serif; }
|
|
19
|
+
.wrap { padding: 4px 2px 8px; display: grid; gap: 8px; }
|
|
20
|
+
.head { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; font-size: 13px; color: var(--muted); }
|
|
21
|
+
.head a { color: var(--accent); text-decoration: none; }
|
|
22
|
+
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 8px; align-items: start; }
|
|
23
|
+
figure.audio { grid-column: 1 / -1; display: flex; align-items: center; gap: 10px; padding: 6px 10px; }
|
|
24
|
+
figure.audio .media { aspect-ratio: auto; background: none; flex: 1; }
|
|
25
|
+
figure.audio .media audio { width: 100%; }
|
|
26
|
+
figure.audio figcaption { order: -1; padding: 0; min-width: 120px; max-width: 40%; color: var(--fg); }
|
|
27
|
+
figure.video { grid-column: span 2; }
|
|
28
|
+
.grid.one { grid-template-columns: minmax(0, 360px); }
|
|
29
|
+
figure { margin: 0; border-radius: 10px; overflow: hidden; border: 1px solid var(--line); background: var(--tile); }
|
|
30
|
+
.media { aspect-ratio: 1; display: grid; place-items: center;
|
|
31
|
+
background: repeating-conic-gradient(var(--check) 0 25%, transparent 0 50%) 50% / 16px 16px; }
|
|
32
|
+
.media img, .media video { width: 100%; height: 100%; object-fit: contain; display: block; image-rendering: auto; }
|
|
33
|
+
.media.pixel img { image-rendering: pixelated; }
|
|
34
|
+
.media.wide { aspect-ratio: 16 / 9; }
|
|
35
|
+
.media audio { width: 92%; }
|
|
36
|
+
figcaption { padding: 5px 8px; font-size: 12px; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
37
|
+
.empty { color: var(--muted); font-size: 13px; padding: 6px 2px; }
|
|
38
|
+
</style>
|
|
39
|
+
</head>
|
|
40
|
+
<body>
|
|
41
|
+
<div class="wrap" id="app"><div class="empty">Waiting for the result…</div></div>
|
|
42
|
+
<script>
|
|
43
|
+
(() => {
|
|
44
|
+
const app = document.getElementById("app");
|
|
45
|
+
let assetLink = "";
|
|
46
|
+
|
|
47
|
+
// ---- host messaging: MCP Apps (JSON-RPC over postMessage) and ChatGPT's window.openai ----
|
|
48
|
+
let nextId = 1;
|
|
49
|
+
const pending = new Map();
|
|
50
|
+
const post = (msg) => window.parent && window.parent.postMessage(msg, "*");
|
|
51
|
+
const request = (method, params) => new Promise((resolve) => {
|
|
52
|
+
const id = nextId++; pending.set(id, resolve); post({ jsonrpc: "2.0", id, method, params });
|
|
53
|
+
});
|
|
54
|
+
const notify = (method, params) => post({ jsonrpc: "2.0", method, params });
|
|
55
|
+
|
|
56
|
+
function openLink(url) {
|
|
57
|
+
if (window.openai && typeof window.openai.openExternal === "function") return window.openai.openExternal({ href: url });
|
|
58
|
+
request("ui/open-link", { url }).catch(() => window.open(url, "_blank", "noopener"));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function applyTheme(theme) {
|
|
62
|
+
if (theme === "dark" || theme === "light") document.documentElement.dataset.theme = theme;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
window.addEventListener("message", (e) => {
|
|
66
|
+
const m = e.data;
|
|
67
|
+
if (!m || m.jsonrpc !== "2.0") return;
|
|
68
|
+
if (m.id && pending.has(m.id)) { pending.get(m.id)(m.result); pending.delete(m.id); return; }
|
|
69
|
+
if (m.method === "ui/notifications/tool-result") render(m.params);
|
|
70
|
+
if (m.method === "ui/notifications/host-context-changed") applyTheme(m.params && m.params.theme);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// ---- rendering ----
|
|
74
|
+
const el = (tag, attrs = {}, kids = []) => {
|
|
75
|
+
const n = document.createElement(tag);
|
|
76
|
+
for (const [k, v] of Object.entries(attrs)) if (v !== undefined && v !== null) n.setAttribute(k, v);
|
|
77
|
+
for (const k of kids) n.append(k);
|
|
78
|
+
return n;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
function tile(item, single) {
|
|
82
|
+
const media = el("div", { class: "media" + (item.kind === "video" ? " wide" : "") });
|
|
83
|
+
if (item.kind === "animation") {
|
|
84
|
+
const img = el("img", { src: item.url, alt: item.label || "" });
|
|
85
|
+
img.addEventListener("load", () => {
|
|
86
|
+
if (img.naturalWidth && img.naturalWidth <= 128) media.classList.add("pixel");
|
|
87
|
+
sizeChanged();
|
|
88
|
+
}, { once: true });
|
|
89
|
+
media.append(img);
|
|
90
|
+
play(img, item.sequence);
|
|
91
|
+
} else if (item.kind === "image" || item.kind === "gif") {
|
|
92
|
+
const img = el("img", { src: item.url, alt: item.label || "", loading: "lazy" });
|
|
93
|
+
img.addEventListener("load", () => {
|
|
94
|
+
if (img.naturalWidth && img.naturalWidth <= 128) media.classList.add("pixel"); // pixel art stays crisp
|
|
95
|
+
sizeChanged();
|
|
96
|
+
});
|
|
97
|
+
media.append(img);
|
|
98
|
+
} else if (item.kind === "video") {
|
|
99
|
+
media.append(el("video", { src: item.url, controls: "", loop: "", muted: "", playsinline: "", autoplay: "" }));
|
|
100
|
+
} else if (item.kind === "audio") {
|
|
101
|
+
media.classList.add("wide");
|
|
102
|
+
media.append(el("audio", { src: item.url, controls: "" }));
|
|
103
|
+
}
|
|
104
|
+
const fig = el("figure", { class: item.kind }, [media]);
|
|
105
|
+
if (item.label) fig.append(el("figcaption", { title: item.label }, [item.label + (item.frames ? ` · ${item.frames} frames` : "")]));
|
|
106
|
+
if (item.link && item.kind !== "audio" && item.kind !== "video") {
|
|
107
|
+
fig.style.cursor = "pointer";
|
|
108
|
+
fig.addEventListener("click", () => openLink(item.link));
|
|
109
|
+
}
|
|
110
|
+
return fig;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Flipbook: preload every frame, then loop at 12 fps (what the app's previews use).
|
|
114
|
+
const FPS = 12;
|
|
115
|
+
const players = [];
|
|
116
|
+
function play(img, urls) {
|
|
117
|
+
const frames = urls.map((u) => { const i = new Image(); i.src = u; return i; });
|
|
118
|
+
players.push({ img, urls, frames, at: 0 });
|
|
119
|
+
}
|
|
120
|
+
setInterval(() => {
|
|
121
|
+
for (const p of players) {
|
|
122
|
+
if (!p.frames.every((f) => f.complete)) continue;
|
|
123
|
+
p.at = (p.at + 1) % p.urls.length;
|
|
124
|
+
p.img.src = p.urls[p.at];
|
|
125
|
+
}
|
|
126
|
+
}, 1000 / FPS);
|
|
127
|
+
|
|
128
|
+
function render(result) {
|
|
129
|
+
const sc = (result && (result.structuredContent || result)) || {};
|
|
130
|
+
const items = Array.isArray(sc.gallery) ? sc.gallery : [];
|
|
131
|
+
app.replaceChildren();
|
|
132
|
+
players.length = 0;
|
|
133
|
+
if (!items.length) { sizeChanged(); return; }
|
|
134
|
+
assetLink = (items.find((i) => i.link) || {}).link || "";
|
|
135
|
+
const head = el("div", { class: "head" }, [el("span", {}, [sc.title || "Result"])]);
|
|
136
|
+
if (assetLink) {
|
|
137
|
+
const a = el("a", { href: assetLink }, ["Open in Voidless ↗"]);
|
|
138
|
+
a.addEventListener("click", (e) => { e.preventDefault(); openLink(assetLink); });
|
|
139
|
+
head.append(a);
|
|
140
|
+
}
|
|
141
|
+
const grid = el("div", { class: "grid" + (items.length === 1 ? " one" : "") }, items.map((i) => tile(i, items.length === 1)));
|
|
142
|
+
app.append(head, grid);
|
|
143
|
+
sizeChanged();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
let lastH = 0;
|
|
147
|
+
function sizeChanged() {
|
|
148
|
+
const h = Math.ceil(document.documentElement.getBoundingClientRect().height);
|
|
149
|
+
if (h === lastH) return;
|
|
150
|
+
lastH = h;
|
|
151
|
+
notify("ui/notifications/size-changed", { height: h });
|
|
152
|
+
}
|
|
153
|
+
new ResizeObserver(sizeChanged).observe(document.body);
|
|
154
|
+
|
|
155
|
+
// ---- start ----
|
|
156
|
+
if (window.openai) {
|
|
157
|
+
applyTheme(window.openai.theme);
|
|
158
|
+
if (window.openai.toolOutput) render({ structuredContent: window.openai.toolOutput });
|
|
159
|
+
window.addEventListener("openai:set_globals", (e) => {
|
|
160
|
+
const g = (e.detail && e.detail.globals) || {};
|
|
161
|
+
if (g.theme) applyTheme(g.theme);
|
|
162
|
+
if (g.toolOutput) render({ structuredContent: g.toolOutput });
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
request("ui/initialize", {
|
|
166
|
+
protocolVersion: "2026-01-26",
|
|
167
|
+
appInfo: { name: "voidless-gallery", version: "1" },
|
|
168
|
+
appCapabilities: {},
|
|
169
|
+
}).then((res) => {
|
|
170
|
+
applyTheme(res && res.hostContext && res.hostContext.theme);
|
|
171
|
+
notify("ui/notifications/initialized", {});
|
|
172
|
+
});
|
|
173
|
+
})();
|
|
174
|
+
</script>
|
|
175
|
+
</body>
|
|
176
|
+
</html>
|