pixelorama-mcp 0.1.0 → 0.2.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.
Files changed (4) hide show
  1. package/README.md +19 -4
  2. package/bridge.js +313 -0
  3. package/package.json +22 -7
  4. package/test.js +0 -9
package/README.md CHANGED
@@ -1,14 +1,29 @@
1
1
  # Pixelorama MCP (Node.js client)
2
2
 
3
+ **Versión:** 0.1.1
4
+
5
+
3
6
  This package provides a **Node.js client** for the **Pixelorama Model Communication Protocol (MCP)** that is implemented inside Pixelorama as a GDScript WebSocket server (see `addons/mcp`).
4
7
 
5
- ## Installation
8
+ ## Instalación
9
+
10
+ ### Desde npm (recomendado)
11
+ ```bash
12
+ # Instalar globalmente para usar el comando en cualquier terminal
13
+ npm i -g pixelorama-mcp
14
+
15
+ # O bien, usar directamente sin instalar con npx
16
+ npx pixelorama-mcp --help
17
+ ```
18
+
19
+ ### Instalación local (para desarrollo)
6
20
  ```bash
7
- npm install --save path/to/Pixelorama/mcp
8
- # or, from the repository root:
9
- npm install ./mcp
21
+ # Desde la raíz del repositorio (en tu máquina)
22
+ npm install ./mcp # instala como dependencia del proyecto
23
+ npm link ./mcp # crea el enlace global `pixelorama-mcp`
10
24
  ```
11
25
 
26
+
12
27
  ## Usage
13
28
  ```js
14
29
  const { PixeloramaMCP } = require('pixelorama-mcp');
package/bridge.js ADDED
@@ -0,0 +1,313 @@
1
+ #!/usr/bin/env node
2
+
3
+ const net = require('net');
4
+ const readline = require('readline');
5
+
6
+ const PIXELORAMA_HOST = process.env.PIXELORAMA_HOST || '127.0.0.1';
7
+ const PIXELORAMA_PORT = parseInt(process.env.PIXELORAMA_PORT || '8080', 10);
8
+
9
+ let tcpClient = null;
10
+ let tcpBuffer = '';
11
+ let tcpPending = new Map();
12
+ let tcpNextId = 1;
13
+
14
+ const TOOLS = [
15
+ {
16
+ name: 'new_project',
17
+ description: 'Create a new empty project in Pixelorama.',
18
+ inputSchema: { type: 'object', properties: {}, required: [] }
19
+ },
20
+ {
21
+ name: 'load_project',
22
+ description: 'Load a Pixelorama project from a file path.',
23
+ inputSchema: {
24
+ type: 'object',
25
+ properties: { path: { type: 'string', description: 'Absolute path to .pxo file' } },
26
+ required: ['path']
27
+ }
28
+ },
29
+ {
30
+ name: 'save_project',
31
+ description: 'Save the current project to a .pxo file.',
32
+ inputSchema: {
33
+ type: 'object',
34
+ properties: { path: { type: 'string', description: 'Absolute path to save .pxo file' } },
35
+ required: ['path']
36
+ }
37
+ },
38
+ {
39
+ name: 'add_frame',
40
+ description: 'Add a new frame to the current project.',
41
+ inputSchema: { type: 'object', properties: {}, required: [] }
42
+ },
43
+ {
44
+ name: 'remove_frame',
45
+ description: 'Remove a frame by index from the current project.',
46
+ inputSchema: {
47
+ type: 'object',
48
+ properties: { index: { type: 'integer', description: 'Frame index (0-based)' } },
49
+ required: ['index']
50
+ }
51
+ },
52
+ {
53
+ name: 'set_frame_duration',
54
+ description: 'Set the duration of a specific frame in seconds.',
55
+ inputSchema: {
56
+ type: 'object',
57
+ properties: {
58
+ index: { type: 'integer', description: 'Frame index (0-based)' },
59
+ seconds: { type: 'number', description: 'Duration in seconds' }
60
+ },
61
+ required: ['index', 'seconds']
62
+ }
63
+ },
64
+ {
65
+ name: 'set_fps',
66
+ description: 'Set the frames per second of the current project.',
67
+ inputSchema: {
68
+ type: 'object',
69
+ properties: { value: { type: 'number', description: 'FPS value (must be > 0)' } },
70
+ required: ['value']
71
+ }
72
+ },
73
+ {
74
+ name: 'toggle_onion',
75
+ description: 'Enable or disable onion skinning.',
76
+ inputSchema: {
77
+ type: 'object',
78
+ properties: { enabled: { type: 'boolean', description: 'true to enable, false to disable' } },
79
+ required: ['enabled']
80
+ }
81
+ },
82
+ {
83
+ name: 'set_onion_past_rate',
84
+ description: 'Set how many past frames are shown in onion skinning.',
85
+ inputSchema: {
86
+ type: 'object',
87
+ properties: { rate: { type: 'integer', description: 'Number of past frames' } },
88
+ required: ['rate']
89
+ }
90
+ },
91
+ {
92
+ name: 'set_onion_future_rate',
93
+ description: 'Set how many future frames are shown in onion skinning.',
94
+ inputSchema: {
95
+ type: 'object',
96
+ properties: { rate: { type: 'integer', description: 'Number of future frames' } },
97
+ required: ['rate']
98
+ }
99
+ },
100
+ {
101
+ name: 'set_onion_opacity',
102
+ description: 'Set onion skinning opacity (0.0 to 1.0).',
103
+ inputSchema: {
104
+ type: 'object',
105
+ properties: { value: { type: 'number', description: 'Opacity value (0.0-1.0)' } },
106
+ required: ['value']
107
+ }
108
+ },
109
+ {
110
+ name: 'set_onion_blue_red',
111
+ description: 'Toggle blue/red onion skinning mode.',
112
+ inputSchema: {
113
+ type: 'object',
114
+ properties: { enabled: { type: 'boolean', description: 'true for blue/red mode' } },
115
+ required: ['enabled']
116
+ }
117
+ },
118
+ {
119
+ name: 'export_project',
120
+ description: 'Export the current project as an image or animation.',
121
+ inputSchema: {
122
+ type: 'object',
123
+ properties: {
124
+ format: { type: 'string', description: 'Export format: PNG, WEBP, JPEG, GIF, APNG, MP4, etc.' },
125
+ output: { type: 'string', description: 'Output file path' }
126
+ },
127
+ required: ['format', 'output']
128
+ }
129
+ },
130
+ {
131
+ name: 'query_status',
132
+ description: 'Get the current project status (frames, fps, onion settings).',
133
+ inputSchema: { type: 'object', properties: {}, required: [] }
134
+ }
135
+ ];
136
+
137
+ const TOOL_TO_COMMAND = {
138
+ new_project: 'new_project',
139
+ load_project: 'load_project',
140
+ save_project: 'save_project',
141
+ add_frame: 'add_frame',
142
+ remove_frame: 'remove_frame',
143
+ set_frame_duration: 'set_frame_duration',
144
+ set_fps: 'set_fps',
145
+ toggle_onion: 'toggle_onion',
146
+ set_onion_past_rate: 'set_onion_past_rate',
147
+ set_onion_future_rate: 'set_onion_future_rate',
148
+ set_onion_opacity: 'set_onion_opacity',
149
+ set_onion_blue_red: 'set_onion_blue_red',
150
+ export_project: 'export',
151
+ query_status: 'query_status'
152
+ };
153
+
154
+ const TOOL_PARAMS_MAP = {
155
+ load_project: (p) => ({ path: p.path }),
156
+ save_project: (p) => ({ path: p.path }),
157
+ remove_frame: (p) => ({ index: p.index }),
158
+ set_frame_duration: (p) => ({ index: p.index, seconds: p.seconds }),
159
+ set_fps: (p) => ({ value: p.value }),
160
+ toggle_onion: (p) => ({ enabled: p.enabled }),
161
+ set_onion_past_rate: (p) => ({ rate: p.rate }),
162
+ set_onion_future_rate: (p) => ({ rate: p.rate }),
163
+ set_onion_opacity: (p) => ({ value: p.value }),
164
+ set_onion_blue_red: (p) => ({ enabled: p.enabled }),
165
+ export_project: (p) => ({ format: p.format, output: p.output })
166
+ };
167
+
168
+ function ensureConnection() {
169
+ return new Promise((resolve, reject) => {
170
+ if (tcpClient && !tcpClient.destroyed && tcpClient.readyState === 'open') {
171
+ return resolve();
172
+ }
173
+ tcpClient = new net.Socket();
174
+ tcpBuffer = '';
175
+ tcpClient.connect(PIXELORAMA_PORT, PIXELORAMA_HOST, () => resolve());
176
+ tcpClient.on('error', (err) => {
177
+ tcpClient = null;
178
+ reject(err);
179
+ });
180
+ tcpClient.on('data', (data) => {
181
+ tcpBuffer += data.toString();
182
+ let idx;
183
+ while ((idx = tcpBuffer.indexOf('\n')) >= 0) {
184
+ const line = tcpBuffer.slice(0, idx);
185
+ tcpBuffer = tcpBuffer.slice(idx + 1);
186
+ if (!line) continue;
187
+ try {
188
+ const msg = JSON.parse(line);
189
+ if (msg.id && tcpPending.has(msg.id)) {
190
+ const { resolve: res, reject: rej } = tcpPending.get(msg.id);
191
+ tcpPending.delete(msg.id);
192
+ if (msg.status === 'ok') res(msg);
193
+ else rej(new Error(msg.message || msg.code || 'Pixelorama error'));
194
+ }
195
+ } catch (_) {}
196
+ }
197
+ });
198
+ tcpClient.on('close', () => { tcpClient = null; });
199
+ });
200
+ }
201
+
202
+ function sendToPixelorama(command, params = {}) {
203
+ return new Promise(async (resolve, reject) => {
204
+ try {
205
+ await ensureConnection();
206
+ } catch (e) {
207
+ return reject(new Error('Cannot connect to Pixelorama on ' + PIXELORAMA_HOST + ':' + PIXELORAMA_PORT + '. Is Pixelorama running with MCP server enabled?'));
208
+ }
209
+ const id = tcpNextId++;
210
+ const payload = Object.assign({ command, id }, params);
211
+ tcpPending.set(id, { resolve, reject });
212
+ try {
213
+ tcpClient.write(JSON.stringify(payload) + '\n');
214
+ } catch (e) {
215
+ tcpPending.delete(id);
216
+ reject(e);
217
+ }
218
+ });
219
+ }
220
+
221
+ function sendJsonRpc(msg) {
222
+ process.stdout.write(JSON.stringify(msg) + '\n');
223
+ }
224
+
225
+ async function handleRequest(msg) {
226
+ if (!msg || !msg.method) {
227
+ sendJsonRpc({ jsonrpc: '2.0', error: { code: -32600, message: 'Invalid Request' }, id: msg?.id ?? null });
228
+ return;
229
+ }
230
+
231
+ const { method, params, id } = msg;
232
+
233
+ if (method === 'initialize') {
234
+ sendJsonRpc({
235
+ jsonrpc: '2.0',
236
+ result: {
237
+ protocolVersion: '2024-11-05',
238
+ capabilities: { tools: { listChanged: false } },
239
+ serverInfo: { name: 'pixelorama-mcp', version: '0.2.0' }
240
+ },
241
+ id
242
+ });
243
+ return;
244
+ }
245
+
246
+ if (method === 'notifications/initialized') {
247
+ return;
248
+ }
249
+
250
+ if (method === 'ping') {
251
+ sendJsonRpc({ jsonrpc: '2.0', result: {}, id });
252
+ return;
253
+ }
254
+
255
+ if (method === 'tools/list') {
256
+ sendJsonRpc({ jsonrpc: '2.0', result: { tools: TOOLS }, id });
257
+ return;
258
+ }
259
+
260
+ if (method === 'tools/call') {
261
+ const toolName = params?.name;
262
+ const toolArgs = params?.arguments || {};
263
+ const cmd = TOOL_TO_COMMAND[toolName];
264
+ if (!cmd) {
265
+ sendJsonRpc({
266
+ jsonrpc: '2.0',
267
+ error: { code: -32601, message: 'Unknown tool: ' + toolName },
268
+ id
269
+ });
270
+ return;
271
+ }
272
+ try {
273
+ const mappedParams = TOOL_PARAMS_MAP[toolName] ? TOOL_PARAMS_MAP[toolName](toolArgs) : {};
274
+ const result = await sendToPixelorama(cmd, mappedParams);
275
+ sendJsonRpc({
276
+ jsonrpc: '2.0',
277
+ result: {
278
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }]
279
+ },
280
+ id
281
+ });
282
+ } catch (err) {
283
+ sendJsonRpc({
284
+ jsonrpc: '2.0',
285
+ result: {
286
+ content: [{ type: 'text', text: 'Error: ' + err.message }],
287
+ isError: true
288
+ },
289
+ id
290
+ });
291
+ }
292
+ return;
293
+ }
294
+
295
+ sendJsonRpc({
296
+ jsonrpc: '2.0',
297
+ error: { code: -32601, message: 'Method not found: ' + method },
298
+ id
299
+ });
300
+ }
301
+
302
+ const rl = readline.createInterface({ input: process.stdin, terminal: false });
303
+ rl.on('line', (line) => {
304
+ const trimmed = line.trim();
305
+ if (!trimmed) return;
306
+ let msg;
307
+ try { msg = JSON.parse(trimmed); } catch (_) { return; }
308
+ handleRequest(msg);
309
+ });
310
+ rl.on('close', () => {
311
+ if (tcpClient) tcpClient.destroy();
312
+ process.exit(0);
313
+ });
package/package.json CHANGED
@@ -1,17 +1,32 @@
1
1
  {
2
2
  "name": "pixelorama-mcp",
3
- "version": "0.1.0",
4
- "description": "Node.js client for Pixelorama Model Communication Protocol (MCP). Allows external scripts to control Pixelorama via WebSocket.",
3
+ "version": "0.2.0",
4
+ "description": "MCP server bridge for Pixelorama exposes Pixelorama tools via the Model Context Protocol (stdio JSON-RPC).",
5
5
  "main": "index.js",
6
6
  "bin": {
7
- "pixelorama-mcp": "cli.js"
7
+ "pixelorama-mcp": "bridge.js",
8
+ "pixelorama-mcp-cli": "cli.js"
8
9
  },
10
+ "files": [
11
+ "bridge.js",
12
+ "cli.js",
13
+ "index.js"
14
+ ],
9
15
  "scripts": {
10
- "test": "node index.js"
16
+ "test": "node test.js",
17
+ "bridge": "node bridge.js"
11
18
  },
12
- "author": "OpenAI",
19
+ "keywords": [
20
+ "mcp",
21
+ "pixelorama",
22
+ "pixel-art",
23
+ "model-context-protocol",
24
+ "stdio"
25
+ ],
26
+ "author": "escobarq",
13
27
  "license": "MIT",
14
- "dependencies": {
15
- "ws": "^8.13.0"
28
+ "dependencies": {},
29
+ "engines": {
30
+ "node": ">=18.0.0"
16
31
  }
17
32
  }
package/test.js DELETED
@@ -1,9 +0,0 @@
1
- const { PixeloramaMCP } = require('./index');
2
- (async () => {
3
- const mcp = new PixeloramaMCP();
4
- await mcp.connect();
5
- console.log('Connected');
6
- const status = await mcp.queryStatus();
7
- console.log('Status:', status);
8
- process.exit(0);
9
- })();