pixelate-mcp 0.3.0 → 0.3.1
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 +76 -28
- package/index.js +467 -136
- package/package.json +5 -7
package/README.md
CHANGED
|
@@ -1,46 +1,94 @@
|
|
|
1
1
|
# pixelate-mcp
|
|
2
2
|
|
|
3
|
-
An [MCP](https://modelcontextprotocol.io) server that
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
An [MCP](https://modelcontextprotocol.io) server that lets an AI coding agent
|
|
4
|
+
(Claude Code, Codex, Cursor) drive the **PixelAOI** editor — see the canvas,
|
|
5
|
+
paint pixel art onto it, run editor commands, pixelate images locally, and
|
|
6
|
+
build node-based blueprint graphs.
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
a subscription key from **https://pixelaoi.pro**.
|
|
8
|
+
Everything runs on **your machine**: the server talks to the editor over a
|
|
9
|
+
localhost-only bridge (`127.0.0.1`). No cloud, no accounts, no API keys.
|
|
10
10
|
|
|
11
|
-
##
|
|
11
|
+
## 1. Enable the bridge in PixelAOI
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
Open PixelAOI and click the **agent** pill in the bottom status bar. It shows
|
|
14
|
+
`A:WAIT` while listening and `A:LIVE` once an agent connects.
|
|
15
15
|
|
|
16
|
+
(Alternatively, launch the editor with the `PIXELAOI_AGENT_BRIDGE=1`
|
|
17
|
+
environment variable set.)
|
|
18
|
+
|
|
19
|
+
## 2. Add the server to your agent
|
|
20
|
+
|
|
21
|
+
### Claude Code
|
|
22
|
+
```
|
|
23
|
+
claude mcp add pixelaoi -- npx -y pixelate-mcp
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Codex (`~/.codex/config.toml`)
|
|
16
27
|
```toml
|
|
17
|
-
[mcp_servers.
|
|
28
|
+
[mcp_servers.pixelaoi]
|
|
18
29
|
command = "npx"
|
|
19
30
|
args = ["-y", "pixelate-mcp"]
|
|
20
|
-
env = { PIXELATE_API_URL = "https://pixelate.pixelaoi.cloud", PIXELATE_API_KEY = "YOUR_KEY" }
|
|
21
31
|
```
|
|
22
32
|
|
|
23
|
-
|
|
24
|
-
|
|
33
|
+
### Claude Desktop / Cursor (JSON config)
|
|
34
|
+
```json
|
|
35
|
+
{
|
|
36
|
+
"command": "npx",
|
|
37
|
+
"args": ["-y", "pixelate-mcp"]
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Restart the agent, make sure the editor is running with the bridge on, and
|
|
42
|
+
ask it to draw something.
|
|
43
|
+
|
|
44
|
+
## Tools
|
|
45
|
+
|
|
46
|
+
Canvas:
|
|
47
|
+
|
|
48
|
+
- **`get_state`** — canvas size, frames, layers, tool, palette, blueprint
|
|
49
|
+
status, and the list of runnable command names.
|
|
50
|
+
- **`get_canvas_image`** — the composited current frame as a PNG.
|
|
51
|
+
- **`run_command`** `{ action }` — run a headless command (`flip_h`,
|
|
52
|
+
`rotate_90`, `trim`, `undo`, `save`, ...); names come from
|
|
53
|
+
`get_state.commands`.
|
|
54
|
+
- **`set_canvas_image`** `{ image_path | png_base64, as_new_document?, name? }`
|
|
55
|
+
— paint an image. Matching canvas size paints onto the open document;
|
|
56
|
+
otherwise it opens as a new one.
|
|
57
|
+
- **`set_pixels`** `{ x, y, w, h, rgba_base64 }` — paint a raw RGBA block,
|
|
58
|
+
clipped to the canvas.
|
|
59
|
+
- **`pixelate_image`** — turn any image (path, base64, or URL) into
|
|
60
|
+
grid-perfect pixel art using the editor's local engine (auto-detects the
|
|
61
|
+
native grid and palette), then paint it in.
|
|
62
|
+
- **`extrude_animate`** — bake a 3D-extrusion turntable of the current canvas
|
|
63
|
+
across the timeline, one keyframe per frame.
|
|
64
|
+
|
|
65
|
+
Blueprints (node graphs):
|
|
25
66
|
|
|
26
|
-
|
|
67
|
+
- **`blueprint_node_kinds`** — the full catalog of addable node kinds.
|
|
68
|
+
- **`blueprint_list`** — nodes, links, and value-wire bindings of the active
|
|
69
|
+
graph.
|
|
70
|
+
- **`blueprint_select_graph`** `{ graph }` — switch between Particle, Canvas,
|
|
71
|
+
Ai, and Layer graphs.
|
|
72
|
+
- **`blueprint_add_node`** / **`blueprint_delete_node`** — add or remove
|
|
73
|
+
nodes.
|
|
74
|
+
- **`blueprint_connect`** — wire node outputs into node inputs.
|
|
75
|
+
- **`blueprint_connect_value`** — drive a node parameter with a value node.
|
|
76
|
+
- **`blueprint_set_param`** / **`blueprint_set_data`** — set continuous
|
|
77
|
+
params or discrete node config.
|
|
78
|
+
- **`blueprint_bake`** — run the graph and bake the result to the canvas.
|
|
27
79
|
|
|
28
|
-
|
|
29
|
-
|-----|-------------|
|
|
30
|
-
| `image_path` / `image_base64` / `image_url` | the source image (provide one) |
|
|
31
|
-
| `auto` | auto-detect grid + palette (default `true`) |
|
|
32
|
-
| `target_width` / `target_height` | output size when `auto` is off or no grid is found |
|
|
33
|
-
| `colors` | palette size (omit to auto-pick) |
|
|
34
|
-
| `seed` | determinism seed |
|
|
35
|
-
| `output_path` | if set, saves the PNG there |
|
|
80
|
+
## Example
|
|
36
81
|
|
|
37
|
-
|
|
82
|
+
> "Take ./hero.png, pixelate it, put it on the canvas, then bake a noise
|
|
83
|
+
> texture behind it with a Canvas blueprint."
|
|
38
84
|
|
|
39
|
-
|
|
85
|
+
The agent calls `pixelate_image` (local engine), then builds
|
|
86
|
+
CanvasNoise -> CanvasOutput with the blueprint tools and runs
|
|
87
|
+
`blueprint_bake`.
|
|
40
88
|
|
|
41
|
-
|
|
42
|
-
- A `PIXELATE_API_KEY` (subscribe at https://pixelaoi.pro)
|
|
89
|
+
## Config
|
|
43
90
|
|
|
44
|
-
|
|
91
|
+
- `PIXELAOI_BRIDGE_HOST` (default `127.0.0.1`)
|
|
92
|
+
- `PIXELAOI_BRIDGE_PORT` (default `48653`)
|
|
45
93
|
|
|
46
|
-
|
|
94
|
+
Get the editor at [pixelaoi.pro](https://pixelaoi.pro).
|
package/index.js
CHANGED
|
@@ -1,136 +1,467 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { readFile, writeFile } from "node:fs/promises";
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
)
|
|
135
|
-
|
|
136
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import net from "node:net";
|
|
3
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
4
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
|
|
8
|
+
const HOST = process.env.PIXELAOI_BRIDGE_HOST || "127.0.0.1";
|
|
9
|
+
const PORT = Number(process.env.PIXELAOI_BRIDGE_PORT || 48653);
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
let sock = null;
|
|
13
|
+
let connected = false;
|
|
14
|
+
let buffer = "";
|
|
15
|
+
let nextId = 1;
|
|
16
|
+
const pending = new Map();
|
|
17
|
+
|
|
18
|
+
function teardown(err) {
|
|
19
|
+
if (sock) {
|
|
20
|
+
try { sock.destroy(); } catch {}
|
|
21
|
+
}
|
|
22
|
+
sock = null;
|
|
23
|
+
connected = false;
|
|
24
|
+
buffer = "";
|
|
25
|
+
for (const [, p] of pending) {
|
|
26
|
+
clearTimeout(p.timer);
|
|
27
|
+
p.reject(err || new Error("bridge disconnected"));
|
|
28
|
+
}
|
|
29
|
+
pending.clear();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function connect() {
|
|
33
|
+
if (connected && sock) return Promise.resolve();
|
|
34
|
+
return new Promise((resolve, reject) => {
|
|
35
|
+
const s = net.createConnection({ host: HOST, port: PORT });
|
|
36
|
+
s.setNoDelay(true);
|
|
37
|
+
s.once("connect", () => {
|
|
38
|
+
sock = s;
|
|
39
|
+
connected = true;
|
|
40
|
+
resolve();
|
|
41
|
+
});
|
|
42
|
+
s.once("error", (e) => {
|
|
43
|
+
connected = false;
|
|
44
|
+
reject(
|
|
45
|
+
new Error(
|
|
46
|
+
`cannot reach the PixelAOI editor bridge at ${HOST}:${PORT} — open the editor and ` +
|
|
47
|
+
`click the "agent" pill in the bottom status bar to enable it ` +
|
|
48
|
+
`(or launch with PIXELAOI_AGENT_BRIDGE=1). (${e.code || e.message})`,
|
|
49
|
+
),
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
s.on("data", (chunk) => {
|
|
53
|
+
buffer += chunk.toString("utf8");
|
|
54
|
+
let nl;
|
|
55
|
+
while ((nl = buffer.indexOf("\n")) !== -1) {
|
|
56
|
+
const line = buffer.slice(0, nl).trim();
|
|
57
|
+
buffer = buffer.slice(nl + 1);
|
|
58
|
+
if (!line) continue;
|
|
59
|
+
let msg;
|
|
60
|
+
try {
|
|
61
|
+
msg = JSON.parse(line);
|
|
62
|
+
} catch {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
const p = pending.get(msg.id);
|
|
66
|
+
if (!p) continue;
|
|
67
|
+
pending.delete(msg.id);
|
|
68
|
+
clearTimeout(p.timer);
|
|
69
|
+
if (msg.ok) p.resolve(msg.result);
|
|
70
|
+
else p.reject(new Error(msg.error || "bridge error"));
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
s.on("close", () => teardown(new Error("bridge connection closed")));
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function call(op, args, timeoutMs = 30000) {
|
|
78
|
+
await connect();
|
|
79
|
+
const id = nextId++;
|
|
80
|
+
return new Promise((resolve, reject) => {
|
|
81
|
+
const timer = setTimeout(() => {
|
|
82
|
+
pending.delete(id);
|
|
83
|
+
reject(new Error(`bridge op '${op}' timed out after ${timeoutMs}ms`));
|
|
84
|
+
}, timeoutMs);
|
|
85
|
+
pending.set(id, { resolve, reject, timer });
|
|
86
|
+
try {
|
|
87
|
+
sock.write(JSON.stringify({ id, op, args: args ?? null }) + "\n");
|
|
88
|
+
} catch (e) {
|
|
89
|
+
clearTimeout(timer);
|
|
90
|
+
pending.delete(id);
|
|
91
|
+
reject(e);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const errText = (e) => ({
|
|
97
|
+
content: [{ type: "text", text: `pixelaoi-mcp error: ${e?.message || e}` }],
|
|
98
|
+
isError: true,
|
|
99
|
+
});
|
|
100
|
+
const stripDataUrl = (s) => {
|
|
101
|
+
const i = s.indexOf(",");
|
|
102
|
+
return s.startsWith("data:") && i !== -1 ? s.slice(i + 1) : s;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const server = new McpServer({ name: "pixelate-mcp", version: "0.3.1" });
|
|
106
|
+
|
|
107
|
+
server.tool(
|
|
108
|
+
"get_state",
|
|
109
|
+
"Get the PixelAOI editor state: canvas size, frame/layer counts, active tool, palette size, blueprint status, and the list of runnable command names.",
|
|
110
|
+
{},
|
|
111
|
+
async () => {
|
|
112
|
+
try {
|
|
113
|
+
const r = await call("state.get");
|
|
114
|
+
return { content: [{ type: "text", text: JSON.stringify(r, null, 2) }] };
|
|
115
|
+
} catch (e) {
|
|
116
|
+
return errText(e);
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
server.tool(
|
|
122
|
+
"get_canvas_image",
|
|
123
|
+
"See the current canvas: returns the composited current frame as a PNG image.",
|
|
124
|
+
{},
|
|
125
|
+
async () => {
|
|
126
|
+
try {
|
|
127
|
+
const r = await call("canvas.get");
|
|
128
|
+
return {
|
|
129
|
+
content: [
|
|
130
|
+
{ type: "text", text: `Canvas ${r.width}x${r.height}.` },
|
|
131
|
+
{ type: "image", data: r.png_base64, mimeType: "image/png" },
|
|
132
|
+
],
|
|
133
|
+
};
|
|
134
|
+
} catch (e) {
|
|
135
|
+
return errText(e);
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
server.tool(
|
|
141
|
+
"run_command",
|
|
142
|
+
"Run a headless editor command by name (e.g. flip_h, flip_v, rotate_90, trim, undo, redo, save). Call get_state first and read its `commands` list for the valid names.",
|
|
143
|
+
{ action: z.string().describe("A command name from get_state.commands") },
|
|
144
|
+
async ({ action }) => {
|
|
145
|
+
try {
|
|
146
|
+
await call("command.run", { action });
|
|
147
|
+
return { content: [{ type: "text", text: `Ran '${action}'.` }] };
|
|
148
|
+
} catch (e) {
|
|
149
|
+
return errText(e);
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
server.tool(
|
|
155
|
+
"set_canvas_image",
|
|
156
|
+
"Paint an image (e.g. a pixelated PNG) into the PixelAOI editor. If the image matches the current canvas size it paints onto the open document; if not, it opens as a NEW document (override with as_new_document). Provide exactly one of image_path or png_base64.",
|
|
157
|
+
{
|
|
158
|
+
image_path: z.string().optional().describe("Local path to a PNG/JPG/WEBP to paint."),
|
|
159
|
+
png_base64: z.string().optional().describe("Base64-encoded image (alternative to image_path)."),
|
|
160
|
+
as_new_document: z
|
|
161
|
+
.boolean()
|
|
162
|
+
.optional()
|
|
163
|
+
.describe("Force a new document (true) or paint onto the current canvas (false). Default: auto by size."),
|
|
164
|
+
name: z.string().optional().describe("Title for the new document, when one is created."),
|
|
165
|
+
},
|
|
166
|
+
async (a) => {
|
|
167
|
+
try {
|
|
168
|
+
let b64 = a.png_base64 ? stripDataUrl(a.png_base64) : undefined;
|
|
169
|
+
if (!b64 && a.image_path) b64 = (await readFile(a.image_path)).toString("base64");
|
|
170
|
+
if (!b64) return errText(new Error("provide image_path or png_base64"));
|
|
171
|
+
const args = { png_base64: b64 };
|
|
172
|
+
if (a.as_new_document != null) args.as_new_document = a.as_new_document;
|
|
173
|
+
if (a.name) args.name = a.name;
|
|
174
|
+
const r = await call("canvas.set_image", args, 60000);
|
|
175
|
+
return {
|
|
176
|
+
content: [{ type: "text", text: `Painted ${r.width}x${r.height} (${r.document} document).` }],
|
|
177
|
+
};
|
|
178
|
+
} catch (e) {
|
|
179
|
+
return errText(e);
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
server.tool(
|
|
185
|
+
"set_pixels",
|
|
186
|
+
"Paint a raw RGBA block onto the current canvas at (x,y), clipped to the canvas bounds. rgba_base64 is base64 of exactly w*h*4 RGBA bytes (row-major, top-left origin).",
|
|
187
|
+
{
|
|
188
|
+
x: z.number().int().describe("Left edge on the canvas."),
|
|
189
|
+
y: z.number().int().describe("Top edge on the canvas."),
|
|
190
|
+
w: z.number().int().positive().describe("Block width in pixels."),
|
|
191
|
+
h: z.number().int().positive().describe("Block height in pixels."),
|
|
192
|
+
rgba_base64: z.string().describe("Base64 of w*h*4 RGBA bytes."),
|
|
193
|
+
},
|
|
194
|
+
async (a) => {
|
|
195
|
+
try {
|
|
196
|
+
await call("canvas.set_pixels", {
|
|
197
|
+
x: a.x,
|
|
198
|
+
y: a.y,
|
|
199
|
+
w: a.w,
|
|
200
|
+
h: a.h,
|
|
201
|
+
rgba_base64: stripDataUrl(a.rgba_base64),
|
|
202
|
+
});
|
|
203
|
+
return { content: [{ type: "text", text: `Painted ${a.w}x${a.h} at (${a.x},${a.y}).` }] };
|
|
204
|
+
} catch (e) {
|
|
205
|
+
return errText(e);
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
server.tool(
|
|
211
|
+
"pixelate_image",
|
|
212
|
+
"Turn an image into grid-perfect pixel art LOCALLY using the editor's built-in engine (auto-detects the native grid + palette), then paint it into PixelAOI. No cloud, no API key. Provide exactly one of image_path, png_base64, or image_url.",
|
|
213
|
+
{
|
|
214
|
+
image_path: z.string().optional().describe("Local path to the source image (png/jpg/webp)."),
|
|
215
|
+
png_base64: z.string().optional().describe("Base64-encoded image (alternative to image_path)."),
|
|
216
|
+
image_url: z.string().optional().describe("URL to fetch the image from."),
|
|
217
|
+
auto: z.boolean().optional().describe("Auto-detect native grid + palette (default true)."),
|
|
218
|
+
colors: z.number().int().positive().optional().describe("Force a palette size; omit for auto."),
|
|
219
|
+
target_width: z.number().int().positive().optional().describe("Force output width (when auto is off)."),
|
|
220
|
+
target_height: z.number().int().positive().optional().describe("Force output height (when auto is off)."),
|
|
221
|
+
seed: z.number().int().optional().describe("Determinism seed (default 42)."),
|
|
222
|
+
output_path: z.string().optional().describe("If set, also save the pixelated PNG here."),
|
|
223
|
+
paint: z.boolean().optional().describe("Paint the result into the editor (default true)."),
|
|
224
|
+
as_new_document: z.boolean().optional().describe("Force new doc (true) or paint onto current (false)."),
|
|
225
|
+
name: z.string().optional().describe("Name for the new document, if created."),
|
|
226
|
+
},
|
|
227
|
+
async (a) => {
|
|
228
|
+
try {
|
|
229
|
+
let b64 = a.png_base64 ? stripDataUrl(a.png_base64) : undefined;
|
|
230
|
+
if (!b64 && a.image_path) b64 = (await readFile(a.image_path)).toString("base64");
|
|
231
|
+
if (!b64 && a.image_url) b64 = Buffer.from(await (await fetch(a.image_url)).arrayBuffer()).toString("base64");
|
|
232
|
+
if (!b64) return errText(new Error("provide image_path, png_base64, or image_url"));
|
|
233
|
+
|
|
234
|
+
const pargs = { png_base64: b64 };
|
|
235
|
+
if (a.auto != null) pargs.auto = a.auto;
|
|
236
|
+
if (a.colors != null) pargs.colors = a.colors;
|
|
237
|
+
if (a.target_width != null) pargs.target_width = a.target_width;
|
|
238
|
+
if (a.target_height != null) pargs.target_height = a.target_height;
|
|
239
|
+
if (a.seed != null) pargs.seed = a.seed;
|
|
240
|
+
const r = await call("canvas.pixelate", pargs, 60000);
|
|
241
|
+
|
|
242
|
+
let summary = `Pixelated to ${r.width}x${r.height}.`;
|
|
243
|
+
if (a.output_path) {
|
|
244
|
+
await writeFile(a.output_path, Buffer.from(r.png_base64, "base64"));
|
|
245
|
+
summary += ` Saved to ${a.output_path}.`;
|
|
246
|
+
}
|
|
247
|
+
if (a.paint !== false) {
|
|
248
|
+
const sargs = { png_base64: r.png_base64 };
|
|
249
|
+
if (a.as_new_document != null) sargs.as_new_document = a.as_new_document;
|
|
250
|
+
if (a.name) sargs.name = a.name;
|
|
251
|
+
const p = await call("canvas.set_image", sargs, 60000);
|
|
252
|
+
summary += ` Painted into PixelAOI (${p.document} document).`;
|
|
253
|
+
}
|
|
254
|
+
return {
|
|
255
|
+
content: [
|
|
256
|
+
{ type: "text", text: summary },
|
|
257
|
+
{ type: "image", data: r.png_base64, mimeType: "image/png" },
|
|
258
|
+
],
|
|
259
|
+
};
|
|
260
|
+
} catch (e) {
|
|
261
|
+
return errText(e);
|
|
262
|
+
}
|
|
263
|
+
},
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
server.tool(
|
|
267
|
+
"extrude_animate",
|
|
268
|
+
"Animate the current canvas as a 3D extrusion turntable: extrudes the silhouette and rotates it across the timeline, baking one keyframe per frame. Builds the frames for you. Great for a heart that turns side to side.",
|
|
269
|
+
{
|
|
270
|
+
frames: z.number().int().min(2).max(64).optional().describe("Timeline frame count (default 16)."),
|
|
271
|
+
from_deg: z.number().optional().describe("Start Y rotation, degrees (default -35)."),
|
|
272
|
+
to_deg: z.number().optional().describe("End Y rotation, degrees (default 35)."),
|
|
273
|
+
depth: z.number().optional().describe("Extrude depth %, default 70."),
|
|
274
|
+
tint: z.boolean().optional().describe("Keep source colors (default true)."),
|
|
275
|
+
rot_x: z.number().optional().describe("Fixed X tilt, degrees (default 12)."),
|
|
276
|
+
ping_pong: z.boolean().optional().describe("Swing back and forth (default true) vs one-way sweep."),
|
|
277
|
+
},
|
|
278
|
+
async (a) => {
|
|
279
|
+
try {
|
|
280
|
+
const args = {};
|
|
281
|
+
for (const k of ["frames", "from_deg", "to_deg", "depth", "tint", "rot_x", "ping_pong"]) {
|
|
282
|
+
if (a[k] != null) args[k] = a[k];
|
|
283
|
+
}
|
|
284
|
+
const r = await call("canvas.extrude_animate", args, 60000);
|
|
285
|
+
return {
|
|
286
|
+
content: [
|
|
287
|
+
{
|
|
288
|
+
type: "text",
|
|
289
|
+
text: `Baked ${r.frames} animation frames (turn ${r.from_deg}° -> ${r.to_deg}°). Play the timeline to see it.`,
|
|
290
|
+
},
|
|
291
|
+
],
|
|
292
|
+
};
|
|
293
|
+
} catch (e) {
|
|
294
|
+
return errText(e);
|
|
295
|
+
}
|
|
296
|
+
},
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
server.tool(
|
|
300
|
+
"blueprint_node_kinds",
|
|
301
|
+
"The COMPLETE catalog of node kinds that can be added to a blueprint graph, grouped by category (use these exact names with blueprint_add_node). This is the full set of addable nodes — NOT the nodes already in the graph (use blueprint_list for that). Defaults to the active graph; pass `graph` to query a specific kind before selecting it.",
|
|
302
|
+
{
|
|
303
|
+
graph: z
|
|
304
|
+
.enum(["Particle", "Canvas", "Ai", "Layer"])
|
|
305
|
+
.optional()
|
|
306
|
+
.describe("Graph kind to query; omit for the active graph."),
|
|
307
|
+
},
|
|
308
|
+
async ({ graph }) => {
|
|
309
|
+
try {
|
|
310
|
+
const r = await call("blueprint.node_kinds", graph ? { graph } : undefined);
|
|
311
|
+
return { content: [{ type: "text", text: JSON.stringify(r, null, 2) }] };
|
|
312
|
+
} catch (e) {
|
|
313
|
+
return errText(e);
|
|
314
|
+
}
|
|
315
|
+
},
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
server.tool(
|
|
319
|
+
"blueprint_list",
|
|
320
|
+
"Inspect the active blueprint graph: its kind, nodes (id, kind, position), links, and per-param value-wire bindings.",
|
|
321
|
+
{},
|
|
322
|
+
async () => {
|
|
323
|
+
try {
|
|
324
|
+
const r = await call("blueprint.list");
|
|
325
|
+
return { content: [{ type: "text", text: JSON.stringify(r, null, 2) }] };
|
|
326
|
+
} catch (e) {
|
|
327
|
+
return errText(e);
|
|
328
|
+
}
|
|
329
|
+
},
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
server.tool(
|
|
333
|
+
"blueprint_select_graph",
|
|
334
|
+
"Switch which blueprint graph is active (Particle, Canvas, Ai, or Layer) and return its summary. Call this before adding nodes so they land in the right graph.",
|
|
335
|
+
{ graph: z.enum(["Particle", "Canvas", "Ai", "Layer"]).describe("Graph kind to make active.") },
|
|
336
|
+
async ({ graph }) => {
|
|
337
|
+
try {
|
|
338
|
+
const r = await call("blueprint.select_graph", { graph });
|
|
339
|
+
return { content: [{ type: "text", text: JSON.stringify(r, null, 2) }] };
|
|
340
|
+
} catch (e) {
|
|
341
|
+
return errText(e);
|
|
342
|
+
}
|
|
343
|
+
},
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
server.tool(
|
|
347
|
+
"blueprint_add_node",
|
|
348
|
+
"Add a node to the active blueprint graph; returns the new node id. `kind` is a NodeKind name, e.g. (Canvas graph) CanvasNoise, CanvasGradient, CanvasShape, CanvasFilter, CanvasColorAdjust, PaletteMap, CanvasOutput; (Particle graph) Emitter, Forces, Lifetime. Use blueprint_list to see what's wireable.",
|
|
349
|
+
{
|
|
350
|
+
kind: z.string().describe("NodeKind name, e.g. CanvasNoise or CanvasOutput."),
|
|
351
|
+
x: z.number().int().optional().describe("Graph X position (default 60)."),
|
|
352
|
+
y: z.number().int().optional().describe("Graph Y position (default 60)."),
|
|
353
|
+
},
|
|
354
|
+
async ({ kind, x, y }) => {
|
|
355
|
+
try {
|
|
356
|
+
const args = { kind };
|
|
357
|
+
if (x != null) args.x = x;
|
|
358
|
+
if (y != null) args.y = y;
|
|
359
|
+
const r = await call("blueprint.add_node", args);
|
|
360
|
+
return { content: [{ type: "text", text: `Added ${kind} as node ${r.id}.` }] };
|
|
361
|
+
} catch (e) {
|
|
362
|
+
return errText(e);
|
|
363
|
+
}
|
|
364
|
+
},
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
server.tool(
|
|
368
|
+
"blueprint_connect",
|
|
369
|
+
"Connect node `from`'s output into node `to`'s input in the active graph (validated by the graph's wiring rules).",
|
|
370
|
+
{
|
|
371
|
+
from: z.number().int().describe("Source node id."),
|
|
372
|
+
to: z.number().int().describe("Destination node id."),
|
|
373
|
+
},
|
|
374
|
+
async ({ from, to }) => {
|
|
375
|
+
try {
|
|
376
|
+
await call("blueprint.connect", { from, to });
|
|
377
|
+
return { content: [{ type: "text", text: `Connected ${from} -> ${to}.` }] };
|
|
378
|
+
} catch (e) {
|
|
379
|
+
return errText(e);
|
|
380
|
+
}
|
|
381
|
+
},
|
|
382
|
+
);
|
|
383
|
+
|
|
384
|
+
server.tool(
|
|
385
|
+
"blueprint_connect_value",
|
|
386
|
+
"Wire a VALUE node (ValueConstant/ValueSine/ValueMath/…) to drive a parameter of another node — the 'value wire' / param-pin connection. `param` is a CanvasAnimParam name (e.g. NoiseScale); omit it to drive the target's primary param.",
|
|
387
|
+
{
|
|
388
|
+
from: z.number().int().describe("Value node id (the driver)."),
|
|
389
|
+
to: z.number().int().describe("Target node id whose param is driven."),
|
|
390
|
+
param: z.string().optional().describe("CanvasAnimParam to drive; omit for the target's primary param."),
|
|
391
|
+
},
|
|
392
|
+
async ({ from, to, param }) => {
|
|
393
|
+
try {
|
|
394
|
+
const args = { from, to };
|
|
395
|
+
if (param) args.param = param;
|
|
396
|
+
await call("blueprint.connect_value", args);
|
|
397
|
+
return { content: [{ type: "text", text: `Value-wired ${from} -> ${to}${param ? " (" + param + ")" : ""}.` }] };
|
|
398
|
+
} catch (e) {
|
|
399
|
+
return errText(e);
|
|
400
|
+
}
|
|
401
|
+
},
|
|
402
|
+
);
|
|
403
|
+
|
|
404
|
+
server.tool(
|
|
405
|
+
"blueprint_set_data",
|
|
406
|
+
"Set a node's discrete config (blend mode, shape kind, source mode, sizes, etc.). First call blueprint_list, copy that node's `data` object, change the field(s) you want, and pass the whole object back here. The data shape must match the node's current one.",
|
|
407
|
+
{
|
|
408
|
+
id: z.number().int().describe("Node id."),
|
|
409
|
+
data: z.record(z.any()).describe("The node's full `data` object (from blueprint_list) with your edits."),
|
|
410
|
+
},
|
|
411
|
+
async ({ id, data }) => {
|
|
412
|
+
try {
|
|
413
|
+
await call("blueprint.set_data", { id, data });
|
|
414
|
+
return { content: [{ type: "text", text: `Updated data on node ${id}.` }] };
|
|
415
|
+
} catch (e) {
|
|
416
|
+
return errText(e);
|
|
417
|
+
}
|
|
418
|
+
},
|
|
419
|
+
);
|
|
420
|
+
|
|
421
|
+
server.tool(
|
|
422
|
+
"blueprint_set_param",
|
|
423
|
+
"Set a continuous node parameter. `param` is a CanvasAnimParam name, e.g. NoiseScale, FilterStrength, GradientAngle, ShapeSize, ThresholdLevel, Hue, OffsetX, OffsetY, Rate, Speed, Lifetime. `value` is a number (clamped to the param's valid range).",
|
|
424
|
+
{
|
|
425
|
+
id: z.number().int().describe("Node id."),
|
|
426
|
+
param: z.string().describe("CanvasAnimParam name, e.g. NoiseScale."),
|
|
427
|
+
value: z.number().describe("New value (clamped to range)."),
|
|
428
|
+
},
|
|
429
|
+
async ({ id, param, value }) => {
|
|
430
|
+
try {
|
|
431
|
+
await call("blueprint.set_param", { id, param, value });
|
|
432
|
+
return { content: [{ type: "text", text: `Set ${param} = ${value} on node ${id}.` }] };
|
|
433
|
+
} catch (e) {
|
|
434
|
+
return errText(e);
|
|
435
|
+
}
|
|
436
|
+
},
|
|
437
|
+
);
|
|
438
|
+
|
|
439
|
+
server.tool(
|
|
440
|
+
"blueprint_delete_node",
|
|
441
|
+
"Delete a node (and any links touching it) from the active graph.",
|
|
442
|
+
{ id: z.number().int().describe("Node id to delete.") },
|
|
443
|
+
async ({ id }) => {
|
|
444
|
+
try {
|
|
445
|
+
await call("blueprint.delete_node", { id });
|
|
446
|
+
return { content: [{ type: "text", text: `Deleted node ${id}.` }] };
|
|
447
|
+
} catch (e) {
|
|
448
|
+
return errText(e);
|
|
449
|
+
}
|
|
450
|
+
},
|
|
451
|
+
);
|
|
452
|
+
|
|
453
|
+
server.tool(
|
|
454
|
+
"blueprint_bake",
|
|
455
|
+
"Run / bake the active blueprint graph to the canvas (or to a layer/timeline, depending on the graph's output node).",
|
|
456
|
+
{},
|
|
457
|
+
async () => {
|
|
458
|
+
try {
|
|
459
|
+
const r = await call("blueprint.bake", undefined, 60000);
|
|
460
|
+
return { content: [{ type: "text", text: `Baked the ${r.baked} graph.` }] };
|
|
461
|
+
} catch (e) {
|
|
462
|
+
return errText(e);
|
|
463
|
+
}
|
|
464
|
+
},
|
|
465
|
+
);
|
|
466
|
+
|
|
467
|
+
await server.connect(new StdioServerTransport());
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pixelate-mcp",
|
|
3
|
-
"version": "0.3.
|
|
4
|
-
"description": "MCP server:
|
|
3
|
+
"version": "0.3.1",
|
|
4
|
+
"description": "MCP server to drive the PixelAOI editor: see the canvas, paint pixel art, pixelate images locally, run editor commands, and build node-based blueprints.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"pixelate-mcp": "index.js"
|
|
@@ -11,17 +11,16 @@
|
|
|
11
11
|
"README.md",
|
|
12
12
|
"LICENSE"
|
|
13
13
|
],
|
|
14
|
+
"homepage": "https://pixelaoi.pro",
|
|
14
15
|
"keywords": [
|
|
15
16
|
"mcp",
|
|
16
17
|
"model-context-protocol",
|
|
17
18
|
"pixel-art",
|
|
19
|
+
"pixelaoi",
|
|
18
20
|
"pixelate",
|
|
19
21
|
"codex",
|
|
20
|
-
"claude"
|
|
21
|
-
"cursor",
|
|
22
|
-
"aseprite"
|
|
22
|
+
"claude"
|
|
23
23
|
],
|
|
24
|
-
"homepage": "https://pixelaoi.pro",
|
|
25
24
|
"license": "MIT",
|
|
26
25
|
"author": "PixelAOI",
|
|
27
26
|
"engines": {
|
|
@@ -29,7 +28,6 @@
|
|
|
29
28
|
},
|
|
30
29
|
"dependencies": {
|
|
31
30
|
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
32
|
-
"sharp": "^0.33.5",
|
|
33
31
|
"zod": "^3.23.8"
|
|
34
32
|
}
|
|
35
33
|
}
|