pixelate-mcp 0.3.1 → 0.6.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/LICENSE +21 -21
- package/README.md +114 -52
- package/index.js +519 -272
- package/package.json +33 -33
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 PixelAOI
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 PixelAOI
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,20 +1,19 @@
|
|
|
1
|
-
#
|
|
1
|
+
# PixelAOI MCP
|
|
2
2
|
|
|
3
|
-
An [MCP](https://modelcontextprotocol.io) server that lets an AI
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
An [MCP](https://modelcontextprotocol.io) server that lets an AI agent (Claude
|
|
4
|
+
Code, Codex, Cursor, Claude Desktop) work in the **PixelAOI** pixel-art editor:
|
|
5
|
+
see the canvas, paint it, arrange layers, build animations, apply filters and
|
|
6
|
+
adjustment layers, pixelate images, and wire Blueprint node graphs.
|
|
7
7
|
|
|
8
8
|
Everything runs on **your machine**: the server talks to the editor over a
|
|
9
|
-
localhost-only
|
|
9
|
+
localhost-only link (`127.0.0.1`). No cloud, no accounts, no API keys.
|
|
10
10
|
|
|
11
|
-
## 1.
|
|
11
|
+
## 1. Turn on the agent link in PixelAOI
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
environment variable set.)
|
|
13
|
+
Click **agent off** in PixelAOI's status bar (bottom right). It turns to
|
|
14
|
+
**agent ready** while it waits, and **agent live** once your agent connects.
|
|
15
|
+
Click it again to turn it off. (Or start PixelAOI with
|
|
16
|
+
`PIXELAOI_AGENT_BRIDGE=1`.)
|
|
18
17
|
|
|
19
18
|
## 2. Add the server to your agent
|
|
20
19
|
|
|
@@ -33,62 +32,125 @@ args = ["-y", "pixelate-mcp"]
|
|
|
33
32
|
### Claude Desktop / Cursor (JSON config)
|
|
34
33
|
```json
|
|
35
34
|
{
|
|
36
|
-
"
|
|
37
|
-
|
|
35
|
+
"mcpServers": {
|
|
36
|
+
"pixelaoi": { "command": "npx", "args": ["-y", "pixelate-mcp"] }
|
|
37
|
+
}
|
|
38
38
|
}
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
-
Restart the agent, make sure
|
|
42
|
-
|
|
41
|
+
Restart the agent, make sure PixelAOI says **agent ready**, and ask it to
|
|
42
|
+
draw something. The server shows up as **PixelAOI**, with the PixelAOI mark
|
|
43
|
+
(in colours for light and dark clients), and tells the agent how to work with
|
|
44
|
+
the editor when it connects.
|
|
43
45
|
|
|
44
46
|
## Tools
|
|
45
47
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
48
|
+
Each tool has a title your client can show, and says whether it only reads or
|
|
49
|
+
changes the document, so a client can let reads through and ask before
|
|
50
|
+
changes. Every change is one undo step in PixelAOI (`run_command` `undo` takes
|
|
51
|
+
it back).
|
|
52
|
+
|
|
53
|
+
| Tool | Title | |
|
|
54
|
+
|---|---|---|
|
|
55
|
+
| `get_state` | Editor state | reads |
|
|
56
|
+
| `get_canvas_image` | See the canvas | reads |
|
|
57
|
+
| `run_command` | Run a command | changes |
|
|
58
|
+
| `apply_filter` | Apply a filter | changes |
|
|
59
|
+
| `layers` | Arrange layers | changes |
|
|
60
|
+
| `adjustment_layer` | Adjustment layers | changes |
|
|
61
|
+
| `set_canvas_image` | Paint an image | changes |
|
|
62
|
+
| `set_pixels` | Paint pixels | changes |
|
|
63
|
+
| `pixelate_image` | Pixelate an image | changes (may fetch a URL) |
|
|
64
|
+
| `extrude_animate` | 3D turntable | changes |
|
|
65
|
+
| `add_frame` | Add a frame | adds |
|
|
66
|
+
| `select_frame` | Show a frame | adds |
|
|
67
|
+
| `set_frame_duration` | Frame timing | changes |
|
|
68
|
+
| `add_tag` | Tag frames | adds |
|
|
69
|
+
| `save_document` | Save the project | changes |
|
|
70
|
+
| `close_document` | Close the document | changes |
|
|
71
|
+
| `blueprint_node_kinds` | Blueprint node kinds | reads |
|
|
72
|
+
| `blueprint_list` | Read the Blueprint | reads |
|
|
73
|
+
| `blueprint_select_graph` | Switch Blueprint graph | adds |
|
|
74
|
+
| `blueprint_add_node` | Add a node | adds |
|
|
75
|
+
| `blueprint_connect` | Connect nodes | adds |
|
|
76
|
+
| `blueprint_connect_value` | Drive a parameter | adds |
|
|
77
|
+
| `blueprint_set_param` | Set a parameter | changes |
|
|
78
|
+
| `blueprint_set_data` | Set node data | changes |
|
|
79
|
+
| `blueprint_delete_node` | Delete a node | changes |
|
|
80
|
+
| `blueprint_bake` | Bake the Blueprint | changes |
|
|
81
|
+
|
|
82
|
+
In more detail:
|
|
83
|
+
|
|
84
|
+
- **`get_state`**: canvas size, frames, the layer stack (each layer's index and
|
|
85
|
+
uid, each folder's id), the selection, the tool, the palette, the Blueprint
|
|
86
|
+
editor's state, and the command names `run_command` takes.
|
|
87
|
+
- **`get_canvas_image`**: the current frame as PixelAOI shows it, as a PNG.
|
|
88
|
+
- **`run_command`** `{ action }`: a command by name (`flip_h`, `rotate_90`,
|
|
89
|
+
`trim`, `undo`, `save`, ...). A filter's name (`invert`, `blur`, ...)
|
|
90
|
+
applies it at once with its remembered settings.
|
|
91
|
+
- **`apply_filter`** `{ name, params?, output?, frames? }`: a filter on the
|
|
92
|
+
active layer as one undo step: `blur`, `hsl_adjust`, `levels`, `outline`,
|
|
93
|
+
`drop_shadow`, `color_reduce`, ... `params` by the labels PixelAOI shows
|
|
94
|
+
(`{"Radius": 3}`, `{"Hue": 30}`) or in order; `output` `pixels` (default),
|
|
95
|
+
`new_layer` or `adjustment` (an adjustment layer doing it live, with
|
|
96
|
+
`affects` `layer` or `below`); `frames` `this` (default), `selected` or
|
|
97
|
+
`all`. A selection limits it. Curves takes points per channel,
|
|
98
|
+
`{"all": [[input, output], ...], "red": ...}`, and Gradient Map a ramp,
|
|
99
|
+
`{"stops": [[position, r, g, b], ...]}` (0-255, at most 16 points or stops).
|
|
100
|
+
- **`adjustment_layer`** `{ action, kind?, params?, affects?, enabled?, item? }`:
|
|
101
|
+
layers with no art of their own that change what is under them, live: `add`,
|
|
102
|
+
`set`, `remove`, `bake`.
|
|
103
|
+
- **`layers`** `{ action, items?, … }`: `select`, `move`, `group`, `ungroup`,
|
|
104
|
+
`new_folder`, `set` (`visible`, `locked`, `opacity`, `blend`, `draft`,
|
|
105
|
+
`clipping`, `expanded`, `name`), `delete`, `duplicate`, `merge`. A layer by
|
|
106
|
+
its index, `{layer: i}` or `{uid: "…"}`; a folder by `{folder: id}`; no
|
|
107
|
+
`items` means the selection.
|
|
108
|
+
- **`set_canvas_image`** `{ image_path | png_base64, as_new_document?, name? }`:
|
|
109
|
+
paint an image; at the canvas's size it paints the open document, otherwise
|
|
110
|
+
it opens as a new one.
|
|
111
|
+
- **`set_pixels`** `{ x, y, w, h, rgba_base64 }`: a raw RGBA block, clipped to
|
|
112
|
+
the canvas.
|
|
113
|
+
- **`pixelate_image`**: any image (path, base64 or URL) into grid-perfect
|
|
114
|
+
pixel art with PixelAOI's own engine (it finds the native grid and palette),
|
|
115
|
+
then painted in.
|
|
116
|
+
- **`extrude_animate`**: a 3D extrusion turntable of the canvas across the
|
|
117
|
+
timeline.
|
|
118
|
+
- **`add_frame`**, **`select_frame`** `{ index }`, **`set_frame_duration`**
|
|
119
|
+
`{ index?, duration_ms }`, **`add_tag`** `{ name, from, to? }`: build an
|
|
120
|
+
animation (frames from 0; 10-10000 ms a frame).
|
|
121
|
+
- **`save_document`** `{ path }`: save as a `.pxl` project at an absolute path;
|
|
122
|
+
**`close_document`**: close the tab once saved (refused while unsaved).
|
|
123
|
+
- **Blueprints**: switch the graph (`Particle`, `Canvas`, `Ai`, `Layer`), list
|
|
124
|
+
the node kinds, add, wire (output to input, or a value node driving a
|
|
125
|
+
parameter), set parameters and node data, delete, and bake the result into
|
|
126
|
+
the document.
|
|
127
|
+
|
|
128
|
+
PixelAOI refuses changes while one of its dialogs or a filter is open, or while
|
|
129
|
+
you are drawing; the error says which, and the call can be made again after.
|
|
79
130
|
|
|
80
131
|
## Example
|
|
81
132
|
|
|
82
133
|
> "Take ./hero.png, pixelate it, put it on the canvas, then bake a noise
|
|
83
134
|
> texture behind it with a Canvas blueprint."
|
|
84
135
|
|
|
85
|
-
The agent calls `pixelate_image
|
|
86
|
-
|
|
87
|
-
`blueprint_bake`.
|
|
136
|
+
The agent calls `pixelate_image`, then builds CanvasNoise → CanvasOutput with
|
|
137
|
+
the Blueprint tools and runs `blueprint_bake`.
|
|
88
138
|
|
|
89
139
|
## Config
|
|
90
140
|
|
|
91
141
|
- `PIXELAOI_BRIDGE_HOST` (default `127.0.0.1`)
|
|
92
142
|
- `PIXELAOI_BRIDGE_PORT` (default `48653`)
|
|
93
143
|
|
|
144
|
+
## If it can't connect
|
|
145
|
+
|
|
146
|
+
The error says so and how to fix it: open PixelAOI and click **agent off** so
|
|
147
|
+
it says **agent ready**. If the editor closes while the agent works, the next
|
|
148
|
+
call reconnects once it is back.
|
|
149
|
+
|
|
150
|
+
## As a Claude Desktop extension
|
|
151
|
+
|
|
152
|
+
`manifest.json` and `icon.png` describe it as a Desktop Extension (its name,
|
|
153
|
+
icon, tools, and the editor's port as a setting). Build the `.mcpb` with
|
|
154
|
+
`npx @anthropic-ai/mcpb pack` in this folder, then open it with Claude Desktop.
|
|
155
|
+
|
|
94
156
|
Get the editor at [pixelaoi.pro](https://pixelaoi.pro).
|
package/index.js
CHANGED
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
// PixelAOI's MCP server: lets an AI agent (Claude Code, Codex, Cursor, Claude
|
|
3
|
+
// Desktop) drive the PixelAOI editor running on this computer. It speaks MCP
|
|
4
|
+
// over stdio and reaches the editor over its agent bridge, a localhost-only
|
|
5
|
+
// TCP link (127.0.0.1:48653) that the user turns on from PixelAOI's status bar.
|
|
2
6
|
import net from "node:net";
|
|
3
7
|
import { readFile, writeFile } from "node:fs/promises";
|
|
4
8
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
9
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
10
|
import { z } from "zod";
|
|
7
11
|
|
|
12
|
+
const VERSION = "0.6.0";
|
|
8
13
|
const HOST = process.env.PIXELAOI_BRIDGE_HOST || "127.0.0.1";
|
|
9
14
|
const PORT = Number(process.env.PIXELAOI_BRIDGE_PORT || 48653);
|
|
10
15
|
|
|
16
|
+
// ── The bridge ─────────────────────────────────────────────────────────────
|
|
17
|
+
// One JSON request per line ({id, op, args}), answered in order
|
|
18
|
+
// ({id, ok, result | error}).
|
|
11
19
|
|
|
12
20
|
let sock = null;
|
|
13
21
|
let connected = false;
|
|
@@ -24,7 +32,7 @@ function teardown(err) {
|
|
|
24
32
|
buffer = "";
|
|
25
33
|
for (const [, p] of pending) {
|
|
26
34
|
clearTimeout(p.timer);
|
|
27
|
-
p.reject(err || new Error("
|
|
35
|
+
p.reject(err || new Error("the link to PixelAOI closed"));
|
|
28
36
|
}
|
|
29
37
|
pending.clear();
|
|
30
38
|
}
|
|
@@ -34,18 +42,26 @@ function connect() {
|
|
|
34
42
|
return new Promise((resolve, reject) => {
|
|
35
43
|
const s = net.createConnection({ host: HOST, port: PORT });
|
|
36
44
|
s.setNoDelay(true);
|
|
45
|
+
let opened = false;
|
|
37
46
|
s.once("connect", () => {
|
|
47
|
+
opened = true;
|
|
38
48
|
sock = s;
|
|
39
49
|
connected = true;
|
|
40
50
|
resolve();
|
|
41
51
|
});
|
|
42
|
-
|
|
52
|
+
// Every error is handled: before the link opens it says how to turn it
|
|
53
|
+
// on; after, the link is torn down (an unhandled one would end the server).
|
|
54
|
+
s.on("error", (e) => {
|
|
55
|
+
if (opened) {
|
|
56
|
+
teardown(new Error(`the link to PixelAOI broke (${e.code || e.message})`));
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
43
59
|
connected = false;
|
|
44
60
|
reject(
|
|
45
61
|
new Error(
|
|
46
|
-
`
|
|
47
|
-
`
|
|
48
|
-
`(
|
|
62
|
+
`can't reach PixelAOI at ${HOST}:${PORT} (${e.code || e.message}). Open PixelAOI and click ` +
|
|
63
|
+
`"agent off" in its status bar: it turns to "agent ready", then "agent live" once this is connected. ` +
|
|
64
|
+
`(Or start PixelAOI with PIXELAOI_AGENT_BRIDGE=1.)`,
|
|
49
65
|
),
|
|
50
66
|
);
|
|
51
67
|
});
|
|
@@ -67,10 +83,10 @@ function connect() {
|
|
|
67
83
|
pending.delete(msg.id);
|
|
68
84
|
clearTimeout(p.timer);
|
|
69
85
|
if (msg.ok) p.resolve(msg.result);
|
|
70
|
-
else p.reject(new Error(msg.error || "
|
|
86
|
+
else p.reject(new Error(msg.error || "PixelAOI refused it"));
|
|
71
87
|
}
|
|
72
88
|
});
|
|
73
|
-
s.on("close", () => teardown(new Error("
|
|
89
|
+
s.on("close", () => teardown(new Error("the link to PixelAOI closed")));
|
|
74
90
|
});
|
|
75
91
|
}
|
|
76
92
|
|
|
@@ -80,7 +96,7 @@ async function call(op, args, timeoutMs = 30000) {
|
|
|
80
96
|
return new Promise((resolve, reject) => {
|
|
81
97
|
const timer = setTimeout(() => {
|
|
82
98
|
pending.delete(id);
|
|
83
|
-
reject(new Error(`
|
|
99
|
+
reject(new Error(`PixelAOI didn't answer '${op}' within ${timeoutMs / 1000}s`));
|
|
84
100
|
}, timeoutMs);
|
|
85
101
|
pending.set(id, { resolve, reject, timer });
|
|
86
102
|
try {
|
|
@@ -93,374 +109,605 @@ async function call(op, args, timeoutMs = 30000) {
|
|
|
93
109
|
});
|
|
94
110
|
}
|
|
95
111
|
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
isError: true,
|
|
99
|
-
});
|
|
112
|
+
const text = (t) => ({ content: [{ type: "text", text: t }] });
|
|
113
|
+
const json = (head, value) => text(head ? `${head}\n${JSON.stringify(value, null, 2)}` : JSON.stringify(value, null, 2));
|
|
100
114
|
const stripDataUrl = (s) => {
|
|
101
115
|
const i = s.indexOf(",");
|
|
102
116
|
return s.startsWith("data:") && i !== -1 ? s.slice(i + 1) : s;
|
|
103
117
|
};
|
|
104
118
|
|
|
105
|
-
|
|
119
|
+
// ── The server ─────────────────────────────────────────────────────────────
|
|
120
|
+
|
|
121
|
+
// The PixelAOI mark: Dark's colours for dark clients, Light's for light ones.
|
|
122
|
+
const ICON_DARK =
|
|
123
|
+
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAvUlEQVR42u3avQ3CMBCA0TPKIIxEQcUoUUrENBSwDSWbmAniE4qsIPy+AfLzJMvROeX8ftRodD+eSmwou35W7/sfYvAAAAAAAMDIpXvscqvNffQ6l7LnC2x9PksAAAAAAAAM3JTto6/Ls32Fed8XyJ5viWoeAAAAAAAAAEiSJMV35wLReW6f1fvcwacwAAAAAAAI5wI/e/7f+zvEEgAAAAAAAOYB/hO0BAAAAAAAgHnA/2UeYAkAAAAAAIDVPvCjNhDw4v7bAAAAAElFTkSuQmCC";
|
|
124
|
+
const ICON_LIGHT =
|
|
125
|
+
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAvklEQVR42u3avQ3CMBCA0TOih3lYJWV6RmAIepdswGDJBDABPqHICsLvGyA/T7IcnVNO9fqKRut8L7Gh7PpZve9/iMEDAAAAAAAjl+6x07w099FHPZc9X2Dr81kCAAAAAABg4I7ZPvq83NpXqPu+QPZ8UyzmAQAAAAAAAIAkSVJ8dy4Qnef2Wb3PHXwKAwAAAACAcC7ws+f/vb9DLAEAAAAAAGAe4D9BSwAAAAAAAJgH/F/mAZYAAAAAAAD42BuQGzL/MDRAkgAAAABJRU5ErkJggg==";
|
|
126
|
+
|
|
127
|
+
const INSTRUCTIONS = `PixelAOI is a pixel-art editor running on this computer; these tools drive it. They reach it over a local link the user turns on in PixelAOI: its status bar says "agent off" until clicked, "agent ready" while it waits, "agent live" once you are connected. If a call can't reach it, ask the user to click that pill.
|
|
128
|
+
- Start with get_state: the canvas size, the frames, the layer stack (with each layer's index and uid), the selection, the active tool and the command names run_command takes. Look with get_canvas_image before and after changes.
|
|
129
|
+
- Every change is one undo step in the editor, with a name; run_command "undo" takes the last one back.
|
|
130
|
+
- Frames count from 0. set_canvas_image and set_pixels paint the active layer of the frame on show; add_frame, select_frame, set_frame_duration and add_tag build animations.
|
|
131
|
+
- Blueprints (node graphs): blueprint_select_graph, blueprint_node_kinds, blueprint_add_node, blueprint_connect / blueprint_connect_value, blueprint_set_param / blueprint_set_data, then blueprint_bake.
|
|
132
|
+
- PixelAOI refuses changes while one of its dialogs or a filter is open, or while the user is drawing; the error says which. Ask the user to finish, then try again.`;
|
|
133
|
+
|
|
134
|
+
const server = new McpServer(
|
|
135
|
+
{
|
|
136
|
+
name: "pixelaoi",
|
|
137
|
+
title: "PixelAOI",
|
|
138
|
+
version: VERSION,
|
|
139
|
+
description:
|
|
140
|
+
"Drive the PixelAOI pixel-art editor: see the canvas, paint it, arrange layers, build animations, apply filters and adjustment layers, and wire Blueprint node graphs.",
|
|
141
|
+
websiteUrl: "https://pixelaoi.pro",
|
|
142
|
+
icons: [
|
|
143
|
+
{ src: ICON_DARK, mimeType: "image/png", sizes: ["64x64"], theme: "dark" },
|
|
144
|
+
{ src: ICON_LIGHT, mimeType: "image/png", sizes: ["64x64"], theme: "light" },
|
|
145
|
+
],
|
|
146
|
+
},
|
|
147
|
+
{ instructions: INSTRUCTIONS },
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
// What a client may show before running a tool: whether it only reads, or
|
|
151
|
+
// changes the document (every change is one undo step in PixelAOI), and
|
|
152
|
+
// whether running it twice does the same as once. Nothing here reaches
|
|
153
|
+
// beyond this computer, except pixelate_image given a URL.
|
|
154
|
+
const READS = { readOnlyHint: true, openWorldHint: false };
|
|
155
|
+
const ADDS = { readOnlyHint: false, destructiveHint: false, openWorldHint: false };
|
|
156
|
+
const CHANGES = { readOnlyHint: false, destructiveHint: true, openWorldHint: false };
|
|
157
|
+
|
|
158
|
+
function tool(name, { title, description, input, annotations }, run) {
|
|
159
|
+
server.registerTool(
|
|
160
|
+
name,
|
|
161
|
+
{ title, description, inputSchema: input ?? {}, annotations: { title, ...annotations } },
|
|
162
|
+
async (args) => {
|
|
163
|
+
try {
|
|
164
|
+
return await run(args ?? {});
|
|
165
|
+
} catch (e) {
|
|
166
|
+
return { content: [{ type: "text", text: `PixelAOI: ${e?.message || e}` }], isError: true };
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ── Seeing ─────────────────────────────────────────────────────────────────
|
|
106
173
|
|
|
107
|
-
|
|
174
|
+
tool(
|
|
108
175
|
"get_state",
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
return { content: [{ type: "text", text: JSON.stringify(r, null, 2) }] };
|
|
115
|
-
} catch (e) {
|
|
116
|
-
return errText(e);
|
|
117
|
-
}
|
|
176
|
+
{
|
|
177
|
+
title: "Editor state",
|
|
178
|
+
description:
|
|
179
|
+
"What is open in PixelAOI: the canvas size, the frames and the one on show, the layer stack (layers and folders, top to bottom, with each layer's index and uid and each folder's id) and the selection, the active tool, the palette size, the Blueprint editor's state, and the command names run_command takes.",
|
|
180
|
+
annotations: READS,
|
|
118
181
|
},
|
|
182
|
+
async () => json("", await call("state.get")),
|
|
119
183
|
);
|
|
120
184
|
|
|
121
|
-
|
|
185
|
+
tool(
|
|
122
186
|
"get_canvas_image",
|
|
123
|
-
|
|
124
|
-
|
|
187
|
+
{
|
|
188
|
+
title: "See the canvas",
|
|
189
|
+
description: "The current frame as PixelAOI shows it (every visible layer together), as a PNG.",
|
|
190
|
+
annotations: READS,
|
|
191
|
+
},
|
|
125
192
|
async () => {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
};
|
|
134
|
-
} catch (e) {
|
|
135
|
-
return errText(e);
|
|
136
|
-
}
|
|
193
|
+
const r = await call("canvas.get");
|
|
194
|
+
return {
|
|
195
|
+
content: [
|
|
196
|
+
{ type: "text", text: `The canvas, ${r.width}×${r.height}.` },
|
|
197
|
+
{ type: "image", data: r.png_base64, mimeType: "image/png" },
|
|
198
|
+
],
|
|
199
|
+
};
|
|
137
200
|
},
|
|
138
201
|
);
|
|
139
202
|
|
|
140
|
-
|
|
203
|
+
// ── Commands, filters, layers ──────────────────────────────────────────────
|
|
204
|
+
|
|
205
|
+
tool(
|
|
141
206
|
"run_command",
|
|
142
|
-
|
|
143
|
-
|
|
207
|
+
{
|
|
208
|
+
title: "Run a command",
|
|
209
|
+
description:
|
|
210
|
+
"Run one of PixelAOI's commands by name: flip_h, flip_v, rotate_90, trim, undo, redo, save and the rest of get_state's commands list. A filter's name (invert, blur, ...) applies it at once with its remembered settings, as one undo step.",
|
|
211
|
+
input: { action: z.string().describe("A command name from get_state's commands.") },
|
|
212
|
+
annotations: CHANGES,
|
|
213
|
+
},
|
|
144
214
|
async ({ action }) => {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
215
|
+
await call("command.run", { action });
|
|
216
|
+
return text(`Ran ${action}.`);
|
|
217
|
+
},
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
tool(
|
|
221
|
+
"apply_filter",
|
|
222
|
+
{
|
|
223
|
+
title: "Apply a filter",
|
|
224
|
+
description:
|
|
225
|
+
"Apply a filter to the active layer, as one undo step named after it: invert, desaturate, posterize, brightness_contrast, hsl_adjust, levels, curves, gradient_map, palettize, blur, pixelate, outline, drop_shadow, despeckle, convolve, convolve_5x5, color_reduce, palette_variants (the user's variant on show; it has no settings). Settings the call leaves out keep the user's remembered (or default) values. A selection limits it, as in the editor. With output adjustment, every filter but color_reduce becomes an adjustment layer instead: the art stays as it is and the change can be edited or applied later (see adjustment_layer).",
|
|
226
|
+
input: {
|
|
227
|
+
name: z.string().describe("The filter, e.g. blur or hsl_adjust."),
|
|
228
|
+
params: z
|
|
229
|
+
.union([z.record(z.any()), z.array(z.number().int())])
|
|
230
|
+
.optional()
|
|
231
|
+
.describe(
|
|
232
|
+
'Settings by the labels PixelAOI shows ({"Radius": 3}, {"Hue": 30, "Saturation": -20}) or in order. Curves (an object only): {"all": [[input, output], ...], "red"/"green"/"blue": ...}, 0-255, at most 16 points a channel; channels left out keep the remembered curve. Gradient Map (an object only): {"stops": [[position, r, g, b], ...]}, 0-255, 1 to 16 stops. Palette Cycling (an object only, adjustment output only): {"runs": [{"colours": [[r, g, b], ...], "every": 1, "reverse": false}]}, at most 8 runs of 64 colours, every 1-16 frames a step. Convolve: "Divide by" 0 = Auto (the sum of the weights), 1 = the Divisor; giving a Divisor selects it. Outline and Drop Shadow take their colour as color: [r, g, b].',
|
|
233
|
+
),
|
|
234
|
+
output: z
|
|
235
|
+
.enum(["pixels", "new_layer", "adjustment"])
|
|
236
|
+
.optional()
|
|
237
|
+
.describe("Change the layer (default), put the result on a new layer above it, or add an adjustment layer doing it live (every filter but color_reduce; palette_cycling only this way)."),
|
|
238
|
+
affects: z
|
|
239
|
+
.enum(["layer", "below"])
|
|
240
|
+
.optional()
|
|
241
|
+
.describe("output adjustment: only the layer under it (default), or everything under it in its folder."),
|
|
242
|
+
frames: z.enum(["this", "selected", "all"]).optional().describe("Which frames (default: the frame on show)."),
|
|
243
|
+
},
|
|
244
|
+
annotations: CHANGES,
|
|
245
|
+
},
|
|
246
|
+
async (a) => {
|
|
247
|
+
const args = { name: a.name };
|
|
248
|
+
if (a.params != null) args.params = a.params;
|
|
249
|
+
if (a.output) args.output = a.output;
|
|
250
|
+
if (a.affects) args.affects = a.affects;
|
|
251
|
+
if (a.frames) args.frames = a.frames;
|
|
252
|
+
const r = await call("filter.apply", args, 60000);
|
|
253
|
+
if (a.output === "adjustment") return text(`${r.undo_label}: adjustment layer ${r.layer} (uid ${r.uid}); the art is unchanged.`);
|
|
254
|
+
return text(r.undo_label ? `Applied ${r.ran} on ${r.frames_written} frame(s) (undo: ${r.undo_label}).` : `${r.ran} changed nothing (no undo step).`);
|
|
151
255
|
},
|
|
152
256
|
);
|
|
153
257
|
|
|
154
|
-
|
|
258
|
+
// A layer by its index (as get_state's stack lists it), {layer: index} or
|
|
259
|
+
// {uid: "…"} (the uid as text, as the stack gives it: it is too big for a JS
|
|
260
|
+
// number); a folder by {folder: id}.
|
|
261
|
+
const stackItem = z.union([
|
|
262
|
+
z.number().int().nonnegative(),
|
|
263
|
+
z.object({ layer: z.number().int().nonnegative() }),
|
|
264
|
+
z.object({ uid: z.union([z.string(), z.number().int().nonnegative()]) }),
|
|
265
|
+
z.object({ folder: z.number().int().nonnegative() }),
|
|
266
|
+
]);
|
|
267
|
+
|
|
268
|
+
tool(
|
|
269
|
+
"layers",
|
|
270
|
+
{
|
|
271
|
+
title: "Arrange layers",
|
|
272
|
+
description:
|
|
273
|
+
"Arrange the layer stack: select, move, group, ungroup, new_folder, set, delete, duplicate or merge. Each change is one undo step and every frame follows. Name a layer by its index (as get_state's stack lists it), {layer: index} or {uid: '…'} (the uid text the stack gives, which survives moves); a folder by {folder: id}. Leaving out items acts on the current selection. Folders hold layers and folders (drawn together, then laid down at the folder's opacity) and may be empty. Locked layers and folders stay put, and nothing moves past a locked layer. Returns the stack (top to bottom) and the selection.",
|
|
274
|
+
input: {
|
|
275
|
+
action: z
|
|
276
|
+
.enum(["select", "move", "group", "ungroup", "new_folder", "set", "delete", "duplicate", "merge"])
|
|
277
|
+
.describe("select: choose items; move: put items at a place; group: items into a new folder where the topmost is; ungroup: a folder away, what it held staying; new_folder: an empty folder (default: above the focus); set: change items; delete; duplicate (each copy right above its original); merge: selected layers into the lowest."),
|
|
278
|
+
items: z.array(stackItem).optional().describe("What to act on (default: the selection)."),
|
|
279
|
+
focus: stackItem.optional().describe("select: the one painted and filtered (default: the last named)."),
|
|
280
|
+
above: stackItem.optional().describe("move / new_folder: right above this item, in its folder."),
|
|
281
|
+
below: stackItem.optional().describe("move / new_folder: right below this item, in its folder."),
|
|
282
|
+
into: z.number().int().nonnegative().nullable().optional().describe("move / new_folder: into this folder (its id), or null for the top level."),
|
|
283
|
+
position: z.enum(["top", "bottom"]).optional().describe("With into: the folder's top (default) or bottom."),
|
|
284
|
+
folder: z.number().int().nonnegative().optional().describe("ungroup: the folder's id."),
|
|
285
|
+
name: z.string().optional().describe("group / new_folder: the new folder's name; set: one item's name."),
|
|
286
|
+
visible: z.boolean().optional().describe("set: shown or hidden (a lighting map: used by lighting or not)."),
|
|
287
|
+
locked: z.boolean().optional().describe("set: locked or not."),
|
|
288
|
+
opacity: z.number().int().min(0).max(255).optional().describe("set: 0-255 (a folder's own opacity for a folder)."),
|
|
289
|
+
blend: z.string().optional().describe("set: a layer's blend mode by name (Normal, Multiply, Screen, Overlay, Soft Light, …)."),
|
|
290
|
+
draft: z.boolean().optional().describe("set: a layer left out of exports."),
|
|
291
|
+
clipping: z.boolean().optional().describe("set: a layer clipped to the one under it."),
|
|
292
|
+
expanded: z.boolean().optional().describe("set: a folder open or folded in the panel."),
|
|
293
|
+
},
|
|
294
|
+
annotations: CHANGES,
|
|
295
|
+
},
|
|
296
|
+
async (a) => {
|
|
297
|
+
const { action, ...rest } = a;
|
|
298
|
+
const args = {};
|
|
299
|
+
for (const [k, v] of Object.entries(rest)) if (v !== undefined) args[k] = v;
|
|
300
|
+
const r = await call(`layer.${action}`, args);
|
|
301
|
+
return json(r.undo_label ? `${r.undo_label} (one undo step).` : "Selected.", { stack: r.stack, selection: r.selection });
|
|
302
|
+
},
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
tool(
|
|
306
|
+
"adjustment_layer",
|
|
307
|
+
{
|
|
308
|
+
title: "Adjustment layers",
|
|
309
|
+
description:
|
|
310
|
+
"Adjustment layers: a layer with no art of its own that changes what is under it, live, and can be changed again or applied any time (the art under it stays as it is). add: a new one right above the layer in focus (its mask: the selection, else everything; one mask for every frame). set: change one's settings, its eye (enabled) or what it affects. remove: delete it. bake: apply it to the pixels of what it changes, in every frame, and remove it (refused while another adjustment under it changes the same layers: bake that one first). Kinds: invert, desaturate, posterize, brightness_contrast, hsl_adjust, levels, curves, gradient_map, palette_variants (a copy of the user's variant on show), palettize (snap to a copy of the active palette), palette_cycling (runs of colours stepping once a frame; give its runs as params), outline and drop_shadow (their colour as params color: [r, g, b]), blur, pixelate, despeckle, convolve, convolve_5x5. A new one takes the focus, and filters refuse an adjustment layer in focus: select an art layer before apply_filter. Its strength is the layer's opacity (layers set). Each call is one undo step. Returns the stack (top to bottom) and the selection.",
|
|
311
|
+
input: {
|
|
312
|
+
action: z.enum(["add", "set", "remove", "bake"]),
|
|
313
|
+
kind: z.string().optional().describe("add: what it does (levels, hsl_adjust, palettize, ...)."),
|
|
314
|
+
params: z
|
|
315
|
+
.union([z.record(z.any()), z.array(z.number().int())])
|
|
316
|
+
.optional()
|
|
317
|
+
.describe('add / set: settings as apply_filter takes them ({"Hue": 30}, Curves points, Gradient Map stops, Palette Cycling runs); set merges them into its own.'),
|
|
318
|
+
affects: z
|
|
319
|
+
.enum(["layer", "below"])
|
|
320
|
+
.optional()
|
|
321
|
+
.describe("add (default layer) / set: only the layer (or folder) right under it, or everything under it in its folder."),
|
|
322
|
+
enabled: z.boolean().optional().describe("set: its eye, on or off."),
|
|
323
|
+
item: stackItem.optional().describe("set / remove / bake: which adjustment layer (default: the one in focus)."),
|
|
324
|
+
},
|
|
325
|
+
annotations: CHANGES,
|
|
326
|
+
},
|
|
327
|
+
async (a) => {
|
|
328
|
+
const args = {};
|
|
329
|
+
for (const [k, v] of Object.entries(a)) if (v !== undefined) args[k] = v;
|
|
330
|
+
const r = await call("layer.adjustment", args, 60000);
|
|
331
|
+
return json(r.undo_label ? `${r.undo_label} (one undo step).` : "Nothing changed.", { stack: r.stack, selection: r.selection });
|
|
332
|
+
},
|
|
333
|
+
);
|
|
334
|
+
|
|
335
|
+
// ── Painting ───────────────────────────────────────────────────────────────
|
|
336
|
+
|
|
337
|
+
tool(
|
|
155
338
|
"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
339
|
{
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
.optional()
|
|
163
|
-
.describe("
|
|
164
|
-
|
|
340
|
+
title: "Paint an image",
|
|
341
|
+
description:
|
|
342
|
+
"Paint an image (a pixelated PNG, say) into PixelAOI. When it matches the canvas size it paints onto the open document; when not, it opens as a NEW document (as_new_document overrides that). Give exactly one of image_path or png_base64.",
|
|
343
|
+
input: {
|
|
344
|
+
image_path: z.string().optional().describe("Local path to a PNG/JPG/WEBP to paint."),
|
|
345
|
+
png_base64: z.string().optional().describe("Base64-encoded image (instead of image_path)."),
|
|
346
|
+
as_new_document: z.boolean().optional().describe("A new document (true) or onto the open canvas (false). Default: by size."),
|
|
347
|
+
name: z.string().optional().describe("The new document's name, when one is made."),
|
|
348
|
+
},
|
|
349
|
+
annotations: CHANGES,
|
|
165
350
|
},
|
|
166
351
|
async (a) => {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
}
|
|
352
|
+
let b64 = a.png_base64 ? stripDataUrl(a.png_base64) : undefined;
|
|
353
|
+
if (!b64 && a.image_path) b64 = (await readFile(a.image_path)).toString("base64");
|
|
354
|
+
if (!b64) throw new Error("give image_path or png_base64");
|
|
355
|
+
const args = { png_base64: b64 };
|
|
356
|
+
if (a.as_new_document != null) args.as_new_document = a.as_new_document;
|
|
357
|
+
if (a.name) args.name = a.name;
|
|
358
|
+
const r = await call("canvas.set_image", args, 60000);
|
|
359
|
+
return text(`Painted ${r.width}×${r.height} (${r.document} document).`);
|
|
181
360
|
},
|
|
182
361
|
);
|
|
183
362
|
|
|
184
|
-
|
|
363
|
+
tool(
|
|
185
364
|
"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
365
|
{
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
366
|
+
title: "Paint pixels",
|
|
367
|
+
description:
|
|
368
|
+
"Paint a raw RGBA block onto the canvas at (x, y), clipped to the canvas. rgba_base64 is base64 of exactly w*h*4 RGBA bytes (row by row, from the top left).",
|
|
369
|
+
input: {
|
|
370
|
+
x: z.number().int().describe("Left edge on the canvas."),
|
|
371
|
+
y: z.number().int().describe("Top edge on the canvas."),
|
|
372
|
+
w: z.number().int().positive().describe("Block width in pixels."),
|
|
373
|
+
h: z.number().int().positive().describe("Block height in pixels."),
|
|
374
|
+
rgba_base64: z.string().describe("Base64 of w*h*4 RGBA bytes."),
|
|
375
|
+
},
|
|
376
|
+
annotations: { ...CHANGES, idempotentHint: true },
|
|
193
377
|
},
|
|
194
378
|
async (a) => {
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
}
|
|
379
|
+
await call("canvas.set_pixels", { x: a.x, y: a.y, w: a.w, h: a.h, rgba_base64: stripDataUrl(a.rgba_base64) });
|
|
380
|
+
return text(`Painted ${a.w}×${a.h} at (${a.x}, ${a.y}).`);
|
|
207
381
|
},
|
|
208
382
|
);
|
|
209
383
|
|
|
210
|
-
|
|
384
|
+
tool(
|
|
211
385
|
"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
386
|
{
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
387
|
+
title: "Pixelate an image",
|
|
388
|
+
description:
|
|
389
|
+
"Turn an image into grid-perfect pixel art with PixelAOI's own engine, on this computer (it finds the native grid and palette), then paint it into PixelAOI. No cloud and no key. Give exactly one of image_path, png_base64 or image_url.",
|
|
390
|
+
input: {
|
|
391
|
+
image_path: z.string().optional().describe("Local path to the source image (png/jpg/webp)."),
|
|
392
|
+
png_base64: z.string().optional().describe("Base64-encoded image (instead of image_path)."),
|
|
393
|
+
image_url: z.string().optional().describe("A URL to fetch the image from."),
|
|
394
|
+
auto: z.boolean().optional().describe("Find the native grid and palette (default true)."),
|
|
395
|
+
colors: z.number().int().positive().optional().describe("A palette size; leave out for auto."),
|
|
396
|
+
target_width: z.number().int().positive().optional().describe("The output width (when auto is off)."),
|
|
397
|
+
target_height: z.number().int().positive().optional().describe("The output height (when auto is off)."),
|
|
398
|
+
seed: z.number().int().optional().describe("Determinism seed (default 42)."),
|
|
399
|
+
output_path: z.string().optional().describe("Also save the pixelated PNG here."),
|
|
400
|
+
paint: z.boolean().optional().describe("Paint the result into PixelAOI (default true)."),
|
|
401
|
+
as_new_document: z.boolean().optional().describe("A new document (true) or onto the open canvas (false)."),
|
|
402
|
+
name: z.string().optional().describe("The new document's name, when one is made."),
|
|
403
|
+
},
|
|
404
|
+
annotations: { ...CHANGES, openWorldHint: true },
|
|
226
405
|
},
|
|
227
406
|
async (a) => {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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);
|
|
407
|
+
let b64 = a.png_base64 ? stripDataUrl(a.png_base64) : undefined;
|
|
408
|
+
if (!b64 && a.image_path) b64 = (await readFile(a.image_path)).toString("base64");
|
|
409
|
+
if (!b64 && a.image_url) b64 = Buffer.from(await (await fetch(a.image_url)).arrayBuffer()).toString("base64");
|
|
410
|
+
if (!b64) throw new Error("give image_path, png_base64 or image_url");
|
|
411
|
+
const pargs = { png_base64: b64 };
|
|
412
|
+
for (const k of ["auto", "colors", "target_width", "target_height", "seed"]) if (a[k] != null) pargs[k] = a[k];
|
|
413
|
+
const r = await call("canvas.pixelate", pargs, 60000);
|
|
414
|
+
let summary = `Pixelated to ${r.width}×${r.height}.`;
|
|
415
|
+
if (a.output_path) {
|
|
416
|
+
await writeFile(a.output_path, Buffer.from(r.png_base64, "base64"));
|
|
417
|
+
summary += ` Saved to ${a.output_path}.`;
|
|
262
418
|
}
|
|
419
|
+
if (a.paint !== false) {
|
|
420
|
+
const sargs = { png_base64: r.png_base64 };
|
|
421
|
+
if (a.as_new_document != null) sargs.as_new_document = a.as_new_document;
|
|
422
|
+
if (a.name) sargs.name = a.name;
|
|
423
|
+
const p = await call("canvas.set_image", sargs, 60000);
|
|
424
|
+
summary += ` Painted into PixelAOI (${p.document} document).`;
|
|
425
|
+
}
|
|
426
|
+
return {
|
|
427
|
+
content: [
|
|
428
|
+
{ type: "text", text: summary },
|
|
429
|
+
{ type: "image", data: r.png_base64, mimeType: "image/png" },
|
|
430
|
+
],
|
|
431
|
+
};
|
|
263
432
|
},
|
|
264
433
|
);
|
|
265
434
|
|
|
266
|
-
|
|
435
|
+
tool(
|
|
267
436
|
"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
437
|
{
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
438
|
+
title: "3D turntable",
|
|
439
|
+
description:
|
|
440
|
+
"Animate the canvas as a 3D extrusion turntable: the silhouette is extruded and turned across the timeline, one frame per step, built for you. A heart that turns side to side, a coin that spins.",
|
|
441
|
+
input: {
|
|
442
|
+
frames: z.number().int().min(2).max(64).optional().describe("How many frames (default 16)."),
|
|
443
|
+
from_deg: z.number().optional().describe("Start turn, degrees (default -35)."),
|
|
444
|
+
to_deg: z.number().optional().describe("End turn, degrees (default 35)."),
|
|
445
|
+
depth: z.number().optional().describe("Extrusion depth %, default 70."),
|
|
446
|
+
tint: z.boolean().optional().describe("Keep the source's colours (default true)."),
|
|
447
|
+
rot_x: z.number().optional().describe("A fixed tilt, degrees (default 12)."),
|
|
448
|
+
ping_pong: z.boolean().optional().describe("Swing back and forth (default true), or one sweep."),
|
|
449
|
+
},
|
|
450
|
+
annotations: CHANGES,
|
|
277
451
|
},
|
|
278
452
|
async (a) => {
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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
|
-
}
|
|
453
|
+
const args = {};
|
|
454
|
+
for (const k of ["frames", "from_deg", "to_deg", "depth", "tint", "rot_x", "ping_pong"]) if (a[k] != null) args[k] = a[k];
|
|
455
|
+
const r = await call("canvas.extrude_animate", args, 60000);
|
|
456
|
+
return text(`Baked ${r.frames} frames (turning ${r.from_deg}° to ${r.to_deg}°). Play the timeline to see it.`);
|
|
296
457
|
},
|
|
297
458
|
);
|
|
298
459
|
|
|
299
|
-
|
|
460
|
+
// ── Blueprints ─────────────────────────────────────────────────────────────
|
|
461
|
+
|
|
462
|
+
const GRAPH = z.enum(["Particle", "Canvas", "Ai", "Layer"]);
|
|
463
|
+
|
|
464
|
+
tool(
|
|
300
465
|
"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
466
|
{
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
.
|
|
306
|
-
|
|
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
|
-
}
|
|
467
|
+
title: "Blueprint node kinds",
|
|
468
|
+
description:
|
|
469
|
+
"Every kind of node a Blueprint graph can take, by category (use these names with blueprint_add_node). This is the catalog, not the nodes in the graph (see blueprint_list). The active graph's, or another kind's with graph.",
|
|
470
|
+
input: { graph: GRAPH.optional().describe("The graph kind to ask about; the active graph's when left out.") },
|
|
471
|
+
annotations: READS,
|
|
315
472
|
},
|
|
473
|
+
async ({ graph }) => json("", await call("blueprint.node_kinds", graph ? { graph } : undefined)),
|
|
316
474
|
);
|
|
317
475
|
|
|
318
|
-
|
|
476
|
+
tool(
|
|
319
477
|
"blueprint_list",
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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
|
-
}
|
|
478
|
+
{
|
|
479
|
+
title: "Read the Blueprint",
|
|
480
|
+
description: "The active Blueprint graph: its kind, its nodes (id, kind, position, data), its links and each parameter's value wire.",
|
|
481
|
+
annotations: READS,
|
|
329
482
|
},
|
|
483
|
+
async () => json("", await call("blueprint.list")),
|
|
330
484
|
);
|
|
331
485
|
|
|
332
|
-
|
|
486
|
+
tool(
|
|
333
487
|
"blueprint_select_graph",
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
return { content: [{ type: "text", text: JSON.stringify(r, null, 2) }] };
|
|
340
|
-
} catch (e) {
|
|
341
|
-
return errText(e);
|
|
342
|
-
}
|
|
488
|
+
{
|
|
489
|
+
title: "Switch Blueprint graph",
|
|
490
|
+
description: "Make a Blueprint graph kind active (Particle, Canvas, Ai or Layer) and return its summary. Do this before adding nodes, so they land in the right graph.",
|
|
491
|
+
input: { graph: GRAPH.describe("The graph kind to make active.") },
|
|
492
|
+
annotations: { ...ADDS, idempotentHint: true },
|
|
343
493
|
},
|
|
494
|
+
async ({ graph }) => json("", await call("blueprint.select_graph", { graph })),
|
|
344
495
|
);
|
|
345
496
|
|
|
346
|
-
|
|
497
|
+
tool(
|
|
347
498
|
"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
499
|
{
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
500
|
+
title: "Add a node",
|
|
501
|
+
description:
|
|
502
|
+
"Add a node to the active Blueprint graph; returns its id. kind is a node kind's name, e.g. (Canvas graph) CanvasNoise, CanvasGradient, CanvasShape, CanvasFilter, CanvasColorAdjust, PaletteMap, CanvasOutput; (Particle graph) Emitter, Forces, Lifetime. See blueprint_node_kinds for them all.",
|
|
503
|
+
input: {
|
|
504
|
+
kind: z.string().describe("The node kind, e.g. CanvasNoise or CanvasOutput."),
|
|
505
|
+
x: z.number().int().optional().describe("Graph X position (default 60)."),
|
|
506
|
+
y: z.number().int().optional().describe("Graph Y position (default 60)."),
|
|
507
|
+
},
|
|
508
|
+
annotations: ADDS,
|
|
353
509
|
},
|
|
354
510
|
async ({ kind, x, y }) => {
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
return { content: [{ type: "text", text: `Added ${kind} as node ${r.id}.` }] };
|
|
361
|
-
} catch (e) {
|
|
362
|
-
return errText(e);
|
|
363
|
-
}
|
|
511
|
+
const args = { kind };
|
|
512
|
+
if (x != null) args.x = x;
|
|
513
|
+
if (y != null) args.y = y;
|
|
514
|
+
const r = await call("blueprint.add_node", args);
|
|
515
|
+
return text(`Added ${kind} as node ${r.id}.`);
|
|
364
516
|
},
|
|
365
517
|
);
|
|
366
518
|
|
|
367
|
-
|
|
519
|
+
tool(
|
|
368
520
|
"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
521
|
{
|
|
371
|
-
|
|
372
|
-
|
|
522
|
+
title: "Connect nodes",
|
|
523
|
+
description: "Wire node from's output into node to's input in the active graph (checked against the graph's rules).",
|
|
524
|
+
input: {
|
|
525
|
+
from: z.number().int().describe("The source node's id."),
|
|
526
|
+
to: z.number().int().describe("The destination node's id."),
|
|
527
|
+
},
|
|
528
|
+
annotations: ADDS,
|
|
373
529
|
},
|
|
374
530
|
async ({ from, to }) => {
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
return { content: [{ type: "text", text: `Connected ${from} -> ${to}.` }] };
|
|
378
|
-
} catch (e) {
|
|
379
|
-
return errText(e);
|
|
380
|
-
}
|
|
531
|
+
await call("blueprint.connect", { from, to });
|
|
532
|
+
return text(`Connected ${from} → ${to}.`);
|
|
381
533
|
},
|
|
382
534
|
);
|
|
383
535
|
|
|
384
|
-
|
|
536
|
+
tool(
|
|
385
537
|
"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
538
|
{
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
539
|
+
title: "Drive a parameter",
|
|
540
|
+
description:
|
|
541
|
+
"Wire a value node (ValueConstant, ValueSine, ValueMath, …) to drive a parameter of another node: a value wire. param is a parameter's name (e.g. NoiseScale); leave it out to drive the target's main one.",
|
|
542
|
+
input: {
|
|
543
|
+
from: z.number().int().describe("The value node's id (the driver)."),
|
|
544
|
+
to: z.number().int().describe("The node whose parameter it drives."),
|
|
545
|
+
param: z.string().optional().describe("The parameter to drive; the target's main one when left out."),
|
|
546
|
+
},
|
|
547
|
+
annotations: ADDS,
|
|
391
548
|
},
|
|
392
549
|
async ({ from, to, param }) => {
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
return { content: [{ type: "text", text: `Value-wired ${from} -> ${to}${param ? " (" + param + ")" : ""}.` }] };
|
|
398
|
-
} catch (e) {
|
|
399
|
-
return errText(e);
|
|
400
|
-
}
|
|
550
|
+
const args = { from, to };
|
|
551
|
+
if (param) args.param = param;
|
|
552
|
+
await call("blueprint.connect_value", args);
|
|
553
|
+
return text(`Value-wired ${from} → ${to}${param ? ` (${param})` : ""}.`);
|
|
401
554
|
},
|
|
402
555
|
);
|
|
403
556
|
|
|
404
|
-
|
|
557
|
+
tool(
|
|
405
558
|
"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
559
|
{
|
|
408
|
-
|
|
409
|
-
|
|
560
|
+
title: "Set node data",
|
|
561
|
+
description:
|
|
562
|
+
"Set a node's settings that aren't numbers (blend mode, shape kind, source mode, sizes, …). Call blueprint_list, copy that node's data object, change the fields you want, and pass the whole object back. Its shape must match the node's.",
|
|
563
|
+
input: {
|
|
564
|
+
id: z.number().int().describe("The node's id."),
|
|
565
|
+
data: z.record(z.any()).describe("The node's whole data object (from blueprint_list), with your changes."),
|
|
566
|
+
},
|
|
567
|
+
annotations: { ...CHANGES, idempotentHint: true },
|
|
410
568
|
},
|
|
411
569
|
async ({ id, data }) => {
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
return { content: [{ type: "text", text: `Updated data on node ${id}.` }] };
|
|
415
|
-
} catch (e) {
|
|
416
|
-
return errText(e);
|
|
417
|
-
}
|
|
570
|
+
await call("blueprint.set_data", { id, data });
|
|
571
|
+
return text(`Updated node ${id}'s data.`);
|
|
418
572
|
},
|
|
419
573
|
);
|
|
420
574
|
|
|
421
|
-
|
|
575
|
+
tool(
|
|
422
576
|
"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
577
|
{
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
578
|
+
title: "Set a parameter",
|
|
579
|
+
description:
|
|
580
|
+
"Set a node's number parameter: NoiseScale, FilterStrength, GradientAngle, ShapeSize, ThresholdLevel, Hue, OffsetX, OffsetY, Rate, Speed, Lifetime, … The value is kept within the parameter's range.",
|
|
581
|
+
input: {
|
|
582
|
+
id: z.number().int().describe("The node's id."),
|
|
583
|
+
param: z.string().describe("The parameter, e.g. NoiseScale."),
|
|
584
|
+
value: z.number().describe("Its new value (kept within its range)."),
|
|
585
|
+
},
|
|
586
|
+
annotations: { ...CHANGES, idempotentHint: true },
|
|
428
587
|
},
|
|
429
588
|
async ({ id, param, value }) => {
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
return { content: [{ type: "text", text: `Set ${param} = ${value} on node ${id}.` }] };
|
|
433
|
-
} catch (e) {
|
|
434
|
-
return errText(e);
|
|
435
|
-
}
|
|
589
|
+
await call("blueprint.set_param", { id, param, value });
|
|
590
|
+
return text(`Set ${param} = ${value} on node ${id}.`);
|
|
436
591
|
},
|
|
437
592
|
);
|
|
438
593
|
|
|
439
|
-
|
|
594
|
+
tool(
|
|
440
595
|
"blueprint_delete_node",
|
|
441
|
-
|
|
442
|
-
|
|
596
|
+
{
|
|
597
|
+
title: "Delete a node",
|
|
598
|
+
description: "Delete a node, and the links touching it, from the active graph.",
|
|
599
|
+
input: { id: z.number().int().describe("The node to delete.") },
|
|
600
|
+
annotations: CHANGES,
|
|
601
|
+
},
|
|
443
602
|
async ({ id }) => {
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
return { content: [{ type: "text", text: `Deleted node ${id}.` }] };
|
|
447
|
-
} catch (e) {
|
|
448
|
-
return errText(e);
|
|
449
|
-
}
|
|
603
|
+
await call("blueprint.delete_node", { id });
|
|
604
|
+
return text(`Deleted node ${id}.`);
|
|
450
605
|
},
|
|
451
606
|
);
|
|
452
607
|
|
|
453
|
-
|
|
608
|
+
tool(
|
|
454
609
|
"blueprint_bake",
|
|
455
|
-
|
|
456
|
-
|
|
610
|
+
{
|
|
611
|
+
title: "Bake the Blueprint",
|
|
612
|
+
description: "Run the active Blueprint graph and bake what it makes into the document: the canvas, a layer or the timeline, as its output node says.",
|
|
613
|
+
annotations: CHANGES,
|
|
614
|
+
},
|
|
457
615
|
async () => {
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
616
|
+
const r = await call("blueprint.bake", undefined, 60000);
|
|
617
|
+
return text(`Baked the ${r.baked} graph.`);
|
|
618
|
+
},
|
|
619
|
+
);
|
|
620
|
+
|
|
621
|
+
// ── Animation and files ────────────────────────────────────────────────────
|
|
622
|
+
|
|
623
|
+
tool(
|
|
624
|
+
"add_frame",
|
|
625
|
+
{
|
|
626
|
+
title: "Add a frame",
|
|
627
|
+
description:
|
|
628
|
+
"Add a blank frame right after the one on show and show it, as the timeline's + does (one undo step). Paint it with set_canvas_image or set_pixels, which write the active layer of the frame on show.",
|
|
629
|
+
annotations: ADDS,
|
|
630
|
+
},
|
|
631
|
+
async () => {
|
|
632
|
+
const r = await call("frame.add");
|
|
633
|
+
return text(`Frame ${r.frame} of ${r.frames} added and on show.`);
|
|
634
|
+
},
|
|
635
|
+
);
|
|
636
|
+
|
|
637
|
+
tool(
|
|
638
|
+
"select_frame",
|
|
639
|
+
{
|
|
640
|
+
title: "Show a frame",
|
|
641
|
+
description: "Show a frame (from 0), as clicking it in the timeline does; the canvas and the painting tools then work on it.",
|
|
642
|
+
input: { index: z.number().int().min(0).describe("The frame, from 0.") },
|
|
643
|
+
annotations: { ...ADDS, idempotentHint: true },
|
|
644
|
+
},
|
|
645
|
+
async ({ index }) => {
|
|
646
|
+
await call("frame.select", { index });
|
|
647
|
+
return text(`Frame ${index} on show.`);
|
|
648
|
+
},
|
|
649
|
+
);
|
|
650
|
+
|
|
651
|
+
tool(
|
|
652
|
+
"set_frame_duration",
|
|
653
|
+
{
|
|
654
|
+
title: "Frame timing",
|
|
655
|
+
description: "How long a frame shows when the animation plays, 10 to 10000 milliseconds; the frame on show when index is left out.",
|
|
656
|
+
input: {
|
|
657
|
+
index: z.number().int().min(0).optional().describe("The frame, from 0; the one on show when left out."),
|
|
658
|
+
duration_ms: z.number().int().min(10).max(10000).describe("Milliseconds."),
|
|
659
|
+
},
|
|
660
|
+
annotations: { ...CHANGES, idempotentHint: true },
|
|
661
|
+
},
|
|
662
|
+
async ({ index, duration_ms }) => {
|
|
663
|
+
const r = await call("frame.set", { index, duration_ms });
|
|
664
|
+
return text(`Frame ${r.frame} shows for ${r.duration_ms} ms.`);
|
|
665
|
+
},
|
|
666
|
+
);
|
|
667
|
+
|
|
668
|
+
tool(
|
|
669
|
+
"add_tag",
|
|
670
|
+
{
|
|
671
|
+
title: "Tag frames",
|
|
672
|
+
description: "Tag a run of frames, from and to included (an animation such as idle or walk), as the timeline does.",
|
|
673
|
+
input: {
|
|
674
|
+
name: z.string().min(1).describe("The tag's name, e.g. idle."),
|
|
675
|
+
from: z.number().int().min(0).describe("The first frame, from 0."),
|
|
676
|
+
to: z.number().int().min(0).optional().describe("The last frame (from, when left out)."),
|
|
677
|
+
},
|
|
678
|
+
annotations: ADDS,
|
|
679
|
+
},
|
|
680
|
+
async ({ name, from, to }) => {
|
|
681
|
+
const r = await call("tag.add", { name, from, to });
|
|
682
|
+
return text(`Tag ${r.tag} '${r.name}' covers frames ${r.from}-${r.to}.`);
|
|
683
|
+
},
|
|
684
|
+
);
|
|
685
|
+
|
|
686
|
+
tool(
|
|
687
|
+
"save_document",
|
|
688
|
+
{
|
|
689
|
+
title: "Save the project",
|
|
690
|
+
description: "Save the open document as a PixelAOI project (.pxl) at an absolute path, without a dialog. It becomes the document's file, as Save As does.",
|
|
691
|
+
input: { path: z.string().describe("An absolute path ending in .pxl.") },
|
|
692
|
+
annotations: { ...CHANGES, idempotentHint: true },
|
|
693
|
+
},
|
|
694
|
+
async ({ path }) => {
|
|
695
|
+
const r = await call("document.save", { path });
|
|
696
|
+
return text(`Saved ${r.saved}.`);
|
|
697
|
+
},
|
|
698
|
+
);
|
|
699
|
+
|
|
700
|
+
tool(
|
|
701
|
+
"close_document",
|
|
702
|
+
{
|
|
703
|
+
title: "Close the document",
|
|
704
|
+
description:
|
|
705
|
+
"Close the open document's tab once it is saved (refused while it has unsaved changes: save_document first). The next tab comes up, or Home after the last.",
|
|
706
|
+
annotations: CHANGES,
|
|
707
|
+
},
|
|
708
|
+
async () => {
|
|
709
|
+
const r = await call("document.close");
|
|
710
|
+
return text(r.home ? "Closed; Home is up." : `Closed; ${r.documents} documents open.`);
|
|
464
711
|
},
|
|
465
712
|
);
|
|
466
713
|
|
package/package.json
CHANGED
|
@@ -1,33 +1,33 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "pixelate-mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "MCP server
|
|
5
|
-
"type": "module",
|
|
6
|
-
"bin": {
|
|
7
|
-
"pixelate-mcp": "index.js"
|
|
8
|
-
},
|
|
9
|
-
"files": [
|
|
10
|
-
"index.js",
|
|
11
|
-
"README.md",
|
|
12
|
-
"LICENSE"
|
|
13
|
-
],
|
|
14
|
-
"homepage": "https://pixelaoi.pro",
|
|
15
|
-
"keywords": [
|
|
16
|
-
"mcp",
|
|
17
|
-
"model-context-protocol",
|
|
18
|
-
"pixel-art",
|
|
19
|
-
"pixelaoi",
|
|
20
|
-
"pixelate",
|
|
21
|
-
"codex",
|
|
22
|
-
"claude"
|
|
23
|
-
],
|
|
24
|
-
"license": "MIT",
|
|
25
|
-
"author": "PixelAOI",
|
|
26
|
-
"engines": {
|
|
27
|
-
"node": ">=18"
|
|
28
|
-
},
|
|
29
|
-
"dependencies": {
|
|
30
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
31
|
-
"zod": "^3.
|
|
32
|
-
}
|
|
33
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "pixelate-mcp",
|
|
3
|
+
"version": "0.6.0",
|
|
4
|
+
"description": "PixelAOI's MCP server: lets an AI agent (Claude Code, Codex, Cursor, Claude Desktop) see the PixelAOI canvas, paint it, arrange layers, build animations, apply filters, pixelate images on your machine and wire Blueprint node graphs.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"pixelate-mcp": "index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"index.js",
|
|
11
|
+
"README.md",
|
|
12
|
+
"LICENSE"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://pixelaoi.pro",
|
|
15
|
+
"keywords": [
|
|
16
|
+
"mcp",
|
|
17
|
+
"model-context-protocol",
|
|
18
|
+
"pixel-art",
|
|
19
|
+
"pixelaoi",
|
|
20
|
+
"pixelate",
|
|
21
|
+
"codex",
|
|
22
|
+
"claude"
|
|
23
|
+
],
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"author": "PixelAOI",
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=18"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@modelcontextprotocol/sdk": "^1.32.1",
|
|
31
|
+
"zod": "^3.25.76"
|
|
32
|
+
}
|
|
33
|
+
}
|