@cyanheads/pixoo-mcp-server 1.2.1 → 1.3.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/AGENTS.md +17 -10
- package/CLAUDE.md +17 -10
- package/Dockerfile +63 -46
- package/README.md +16 -12
- package/changelog/1.2.x/1.2.2.md +40 -0
- package/changelog/1.3.x/1.3.0.md +35 -0
- package/dist/config/server-config.d.ts +1 -1
- package/dist/config/server-config.d.ts.map +1 -1
- package/dist/config/server-config.js +6 -1
- package/dist/config/server-config.js.map +1 -1
- package/dist/mcp-server/resources/definitions/pixoo-design-guide.resource.d.ts.map +1 -1
- package/dist/mcp-server/resources/definitions/pixoo-design-guide.resource.js +4 -1
- package/dist/mcp-server/resources/definitions/pixoo-design-guide.resource.js.map +1 -1
- package/dist/mcp-server/tools/asset-source-schema.d.ts +13 -0
- package/dist/mcp-server/tools/asset-source-schema.d.ts.map +1 -0
- package/dist/mcp-server/tools/asset-source-schema.js +25 -0
- package/dist/mcp-server/tools/asset-source-schema.js.map +1 -0
- package/dist/mcp-server/tools/definitions/pixoo-compose-scene.tool.d.ts +139 -55
- package/dist/mcp-server/tools/definitions/pixoo-compose-scene.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/pixoo-compose-scene.tool.js +182 -79
- package/dist/mcp-server/tools/definitions/pixoo-compose-scene.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/pixoo-design-brief.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/pixoo-design-brief.tool.js +14 -6
- package/dist/mcp-server/tools/definitions/pixoo-design-brief.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/pixoo-discover-devices.tool.d.ts +1 -1
- package/dist/mcp-server/tools/definitions/pixoo-discover-devices.tool.js +1 -1
- package/dist/mcp-server/tools/definitions/pixoo-discover-devices.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/pixoo-display-text.tool.d.ts +8 -9
- package/dist/mcp-server/tools/definitions/pixoo-display-text.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/pixoo-display-text.tool.js +50 -29
- package/dist/mcp-server/tools/definitions/pixoo-display-text.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/pixoo-overlay-text.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/pixoo-overlay-text.tool.js +1 -3
- package/dist/mcp-server/tools/definitions/pixoo-overlay-text.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/pixoo-push-image.tool.d.ts +30 -1
- package/dist/mcp-server/tools/definitions/pixoo-push-image.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/pixoo-push-image.tool.js +106 -28
- package/dist/mcp-server/tools/definitions/pixoo-push-image.tool.js.map +1 -1
- package/dist/mcp-server/tools/finish-schema.d.ts +28 -0
- package/dist/mcp-server/tools/finish-schema.d.ts.map +1 -0
- package/dist/mcp-server/tools/finish-schema.js +42 -0
- package/dist/mcp-server/tools/finish-schema.js.map +1 -0
- package/dist/renderer/finish.d.ts +34 -0
- package/dist/renderer/finish.d.ts.map +1 -0
- package/dist/renderer/finish.js +62 -0
- package/dist/renderer/finish.js.map +1 -0
- package/dist/renderer/keyframes.d.ts +5 -1
- package/dist/renderer/keyframes.d.ts.map +1 -1
- package/dist/renderer/keyframes.js +16 -2
- package/dist/renderer/keyframes.js.map +1 -1
- package/dist/renderer/remote-image.d.ts +9 -7
- package/dist/renderer/remote-image.d.ts.map +1 -1
- package/dist/renderer/remote-image.js +15 -23
- package/dist/renderer/remote-image.js.map +1 -1
- package/dist/renderer/scene-renderer.d.ts +59 -14
- package/dist/renderer/scene-renderer.d.ts.map +1 -1
- package/dist/renderer/scene-renderer.js +204 -66
- package/dist/renderer/scene-renderer.js.map +1 -1
- package/dist/renderer/text-engine.d.ts +40 -11
- package/dist/renderer/text-engine.d.ts.map +1 -1
- package/dist/renderer/text-engine.js +75 -40
- package/dist/renderer/text-engine.js.map +1 -1
- package/dist/services/pixoo/pixoo-service.d.ts.map +1 -1
- package/dist/services/pixoo/pixoo-service.js +9 -33
- package/dist/services/pixoo/pixoo-service.js.map +1 -1
- package/package.json +3 -3
- package/server.json +3 -3
package/AGENTS.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Developer Protocol
|
|
2
2
|
|
|
3
3
|
**Server:** pixoo-mcp-server
|
|
4
|
-
**Version:** 1.
|
|
5
|
-
**Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.
|
|
4
|
+
**Version:** 1.3.0
|
|
5
|
+
**Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.10`
|
|
6
6
|
**Engines:** Bun ≥1.4.0, Node ≥24.0.0
|
|
7
7
|
**MCP SDK:** `@modelcontextprotocol/server` ^2.1.0 (protocol revision 2026-07-28 alongside the 2025 era)
|
|
8
8
|
**Zod:** ^4.6.5
|
|
@@ -118,6 +118,7 @@ export const pixooDeviceStatusResource = resource('pixoo://device/status', {
|
|
|
118
118
|
|
|
119
119
|
```ts
|
|
120
120
|
// src/config/server-config.ts
|
|
121
|
+
import * as path from 'node:path';
|
|
121
122
|
import { z } from '@cyanheads/mcp-ts-core';
|
|
122
123
|
import { parseEnvConfig } from '@cyanheads/mcp-ts-core/config';
|
|
123
124
|
|
|
@@ -126,7 +127,8 @@ const ServerConfigSchema = z.object({
|
|
|
126
127
|
pixooSize: z.coerce.number()
|
|
127
128
|
.refine((v) => v === 16 || v === 32 || v === 64)
|
|
128
129
|
.default(64).describe('Display size in pixels (16, 32, or 64).'),
|
|
129
|
-
pixooOutputDir: z.string().
|
|
130
|
+
pixooOutputDir: z.string().transform((dir) => path.resolve(dir)).optional()
|
|
131
|
+
.describe('Auto-save directory for preview PNG/GIF files; a relative path resolves against the launch directory.'),
|
|
130
132
|
pixooPushMinIntervalMs: z.coerce.number().int().min(0).default(1000)
|
|
131
133
|
.describe('Pacing floor between device pushes in milliseconds.'),
|
|
132
134
|
});
|
|
@@ -164,12 +166,11 @@ Handlers receive a unified `ctx` object. Key properties used by this server:
|
|
|
164
166
|
|:---------|:------------|
|
|
165
167
|
| `ctx.log` | Request-scoped logger — `.debug()`, `.info()`, `.notice()`, `.warning()`, `.error()`. Auto-correlates requestId, traceId, tenantId. Dual-sink: Pino **and** `notifications/message` to the client, so treat it as client-visible. |
|
|
166
168
|
| `ctx.enrich` | Success-path agent context — `.notice()` / `.total()` / `.echo()` / `.truncated()`. Lands only when the definition declares an `enrichment` block. |
|
|
167
|
-
| `ctx.fail` | Typed throw against the definition's `errors[]` reason union — auto-populates `data.reason`. |
|
|
168
|
-
| `ctx.recoveryFor` | `{ recovery: { hint } }` for a declared reason, resolved from the contract. Pass it as `ctx.fail`'s data argument (or spread it in) to put the declared hint on the wire. |
|
|
169
|
+
| `ctx.fail` | Typed throw against the definition's `errors[]` reason union — auto-populates `data.reason`; the framework fills in the entry's `recovery` as `data.recovery.hint`. |
|
|
169
170
|
| `ctx.state` | Tenant-scoped KV — `.get`, `.set(key, value, { ttl? })`, `.delete`, `.getMany`, `.list`. Accepts any JSON-serializable value; reads return its JSON form (a `Date` comes back as an ISO string). Keys are validated; colons are not legal separators. |
|
|
170
171
|
| `ctx.requestInput` / `ctx.inputs` | Multi-round-trip input. `return ctx.requestInput(...)`; read the answers with `ctx.inputs.accepted(key, schema)` on re-entry. Unused by this server. |
|
|
171
172
|
| `ctx.signal` | `AbortSignal` for cancellation. Pass it to any long-running I/O. |
|
|
172
|
-
| `ctx.requestId` |
|
|
173
|
+
| `ctx.requestId` | Request ID — the one every log record of the call carries and its error envelope returns as `data.requestId`. |
|
|
173
174
|
| `ctx.tenantId` | Tenant ID from JWT; `'default'` for stdio or HTTP with auth off. |
|
|
174
175
|
|
|
175
176
|
---
|
|
@@ -187,11 +188,12 @@ Pixoo-specific error reasons declared on tools:
|
|
|
187
188
|
| `device_rejected` | `ServiceUnavailable` | Firmware returned non-zero `error_code` | — |
|
|
188
189
|
| `no_device_configured` | `InvalidParams` | Device tool called without `PIXOO_IP` | — |
|
|
189
190
|
| `asset_not_found` | `NotFound` | Image/sprite path or URL unreadable | — |
|
|
191
|
+
| `invalid_image` | `InvalidParams` | Image source or sprite path read but not decodable | — |
|
|
190
192
|
| `invalid_color` | `InvalidParams` | `resolveColor` throw — invalid color name or format | — |
|
|
191
193
|
| `unknown_icon` | `InvalidParams` | Icon name not in registry | — |
|
|
192
194
|
| `discovery_failed` | `ServiceUnavailable` | Divoom cloud unreachable | `true` |
|
|
193
195
|
|
|
194
|
-
`PixooService` and `src/renderer/` raise the device, configuration, and asset reasons themselves (a factory error carrying `data.reason`), so those entries carry `thrownBy: 'service'` — lint-only metadata that keeps `error-contract-unthrown` from reading them as dead. A reason the handler throws with `ctx.fail` stays unmarked
|
|
196
|
+
`PixooService` and `src/renderer/` raise the device, configuration, and asset reasons themselves (a factory error carrying `data.reason`), so those entries carry `thrownBy: 'service'` — lint-only metadata that keeps `error-contract-unthrown` from reading them as dead. A reason the handler throws with `ctx.fail` stays unmarked. Either way, the framework puts the entry's `recovery` on the wire when a failure carrying that `reason` arrives without a hint — a bare `ctx.fail('reason', message)` or a service throw with `data: { reason }` — as `data.recovery.hint`, mirrored into `content[]` unless the message already contains it. Pass an explicit `{ recovery: { hint } }` only to override it for one occurrence — a throw-site hint wins over the contract, so wording that holds for every occurrence of a reason lives in its `errors[]` entry, not at the throw. Every error envelope also carries `data.requestId`, and `content[]` closes with `(reason … · request <id>)`.
|
|
195
197
|
|
|
196
198
|
A contract's `retryable` reaches the wire only through `ctx.fail`; a service throw carries it only when the service writes `data.retryable` itself. `classifyDeviceFailure` (in `pixoo-service.ts`) is the one place a failed device call becomes a reason and a retryability — `PixooService` writes both on its push-path throws, and `pixoo_overlay_text` passes the retryability into `ctx.fail` so it overrides the contract default per occurrence.
|
|
197
199
|
|
|
@@ -211,7 +213,7 @@ errors: [
|
|
|
211
213
|
],
|
|
212
214
|
|
|
213
215
|
// in the handler
|
|
214
|
-
throw ctx.fail('invalid_color', `Invalid color "${input.color}"
|
|
216
|
+
throw ctx.fail('invalid_color', `Invalid color "${input.color}"`);
|
|
215
217
|
```
|
|
216
218
|
|
|
217
219
|
Baseline codes (`InternalError`, `ServiceUnavailable`, `Timeout`, `ValidationError`, `SerializationError`, `RequestCancelled`) bubble freely and don't need declaring. A tool argument that fails the input schema reaches the client as `InvalidParams` (-32602) with `structuredContent.error` — assert that code, not `ValidationError`, in tests.
|
|
@@ -234,11 +236,14 @@ src/
|
|
|
234
236
|
text-engine.ts # Gradient ramp + shadow + outline text engine, overflow handling
|
|
235
237
|
scene-renderer.ts # Element vocabulary, layout resolver, frame rendering
|
|
236
238
|
keyframes.ts # Keyframe interpolation + animation preset compiler
|
|
239
|
+
finish.ts # Palette finish (quantize + dither) for one frame, or frames sharing one palette
|
|
237
240
|
preview.ts # PNG/contact-sheet/GIF encoding
|
|
238
|
-
remote-image.ts # https image fetch
|
|
241
|
+
remote-image.ts # https image fetch into memory for the toolkit loader; stops on ctx.signal
|
|
239
242
|
mcp-server/
|
|
240
243
|
tools/
|
|
244
|
+
asset-source-schema.ts # Shared image `source` / sprite `path` schemas: absolute path or URL only
|
|
241
245
|
device-push.ts # Shared post-render push: preview kept on failure, visibility notice
|
|
246
|
+
finish-schema.ts # Shared `finish` input schema (colors | palette, plus dither)
|
|
242
247
|
tools/definitions/
|
|
243
248
|
pixoo-display-text.tool.ts
|
|
244
249
|
pixoo-compose-scene.tool.ts
|
|
@@ -254,6 +259,8 @@ src/
|
|
|
254
259
|
pixoo-design-guide.resource.ts
|
|
255
260
|
tests/
|
|
256
261
|
index.session-mode.test.ts # Boots the entry point over HTTP, pins the declared session mode
|
|
262
|
+
config/ # Server config parsed from env vars
|
|
263
|
+
helpers/ # Shared fixtures and assertions (image sources, canvas ink, device failures)
|
|
257
264
|
renderer/ # Pure renderer unit tests (no device)
|
|
258
265
|
resources/ # Resource handler tests
|
|
259
266
|
services/pixoo/ # PixooService tests with a fake client
|
|
@@ -375,7 +382,7 @@ Each per-version file opens with YAML frontmatter: `summary` (required, ≤350 c
|
|
|
375
382
|
## Checklist
|
|
376
383
|
|
|
377
384
|
- [ ] Zod schemas: all fields have `.describe()`, only JSON-Schema-serializable types (no `z.custom()`, `z.date()`, `z.transform()`, `z.bigint()`, `z.symbol()`, `z.void()`, `z.map()`, `z.set()`, `z.function()`, `z.nan()`)
|
|
378
|
-
- [ ] Tool inputs are
|
|
385
|
+
- [ ] Tool inputs are closed at every level — the framework closes the root `z.object`, and every nested input object is a `z.strictObject`, so an undeclared key fails by its full path (`Unknown key style.colour. style accepts: …`) instead of being dropped. A tool with nested input objects gets a schema test that walks its `input` and fails on any open object below the root (`tests/helpers/zod-object-nodes.ts`). Use `z.looseObject()` / `.catchall()` only where an open object is genuinely required
|
|
379
386
|
- [ ] JSDoc `@fileoverview` + `@module` on every file
|
|
380
387
|
- [ ] `ctx.log` for logging, `ctx.state` for storage
|
|
381
388
|
- [ ] Handlers throw on failure — error factories or plain `Error`, no try/catch
|
package/CLAUDE.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Developer Protocol
|
|
2
2
|
|
|
3
3
|
**Server:** pixoo-mcp-server
|
|
4
|
-
**Version:** 1.
|
|
5
|
-
**Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.
|
|
4
|
+
**Version:** 1.3.0
|
|
5
|
+
**Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.10`
|
|
6
6
|
**Engines:** Bun ≥1.4.0, Node ≥24.0.0
|
|
7
7
|
**MCP SDK:** `@modelcontextprotocol/server` ^2.1.0 (protocol revision 2026-07-28 alongside the 2025 era)
|
|
8
8
|
**Zod:** ^4.6.5
|
|
@@ -118,6 +118,7 @@ export const pixooDeviceStatusResource = resource('pixoo://device/status', {
|
|
|
118
118
|
|
|
119
119
|
```ts
|
|
120
120
|
// src/config/server-config.ts
|
|
121
|
+
import * as path from 'node:path';
|
|
121
122
|
import { z } from '@cyanheads/mcp-ts-core';
|
|
122
123
|
import { parseEnvConfig } from '@cyanheads/mcp-ts-core/config';
|
|
123
124
|
|
|
@@ -126,7 +127,8 @@ const ServerConfigSchema = z.object({
|
|
|
126
127
|
pixooSize: z.coerce.number()
|
|
127
128
|
.refine((v) => v === 16 || v === 32 || v === 64)
|
|
128
129
|
.default(64).describe('Display size in pixels (16, 32, or 64).'),
|
|
129
|
-
pixooOutputDir: z.string().
|
|
130
|
+
pixooOutputDir: z.string().transform((dir) => path.resolve(dir)).optional()
|
|
131
|
+
.describe('Auto-save directory for preview PNG/GIF files; a relative path resolves against the launch directory.'),
|
|
130
132
|
pixooPushMinIntervalMs: z.coerce.number().int().min(0).default(1000)
|
|
131
133
|
.describe('Pacing floor between device pushes in milliseconds.'),
|
|
132
134
|
});
|
|
@@ -164,12 +166,11 @@ Handlers receive a unified `ctx` object. Key properties used by this server:
|
|
|
164
166
|
|:---------|:------------|
|
|
165
167
|
| `ctx.log` | Request-scoped logger — `.debug()`, `.info()`, `.notice()`, `.warning()`, `.error()`. Auto-correlates requestId, traceId, tenantId. Dual-sink: Pino **and** `notifications/message` to the client, so treat it as client-visible. |
|
|
166
168
|
| `ctx.enrich` | Success-path agent context — `.notice()` / `.total()` / `.echo()` / `.truncated()`. Lands only when the definition declares an `enrichment` block. |
|
|
167
|
-
| `ctx.fail` | Typed throw against the definition's `errors[]` reason union — auto-populates `data.reason`. |
|
|
168
|
-
| `ctx.recoveryFor` | `{ recovery: { hint } }` for a declared reason, resolved from the contract. Pass it as `ctx.fail`'s data argument (or spread it in) to put the declared hint on the wire. |
|
|
169
|
+
| `ctx.fail` | Typed throw against the definition's `errors[]` reason union — auto-populates `data.reason`; the framework fills in the entry's `recovery` as `data.recovery.hint`. |
|
|
169
170
|
| `ctx.state` | Tenant-scoped KV — `.get`, `.set(key, value, { ttl? })`, `.delete`, `.getMany`, `.list`. Accepts any JSON-serializable value; reads return its JSON form (a `Date` comes back as an ISO string). Keys are validated; colons are not legal separators. |
|
|
170
171
|
| `ctx.requestInput` / `ctx.inputs` | Multi-round-trip input. `return ctx.requestInput(...)`; read the answers with `ctx.inputs.accepted(key, schema)` on re-entry. Unused by this server. |
|
|
171
172
|
| `ctx.signal` | `AbortSignal` for cancellation. Pass it to any long-running I/O. |
|
|
172
|
-
| `ctx.requestId` |
|
|
173
|
+
| `ctx.requestId` | Request ID — the one every log record of the call carries and its error envelope returns as `data.requestId`. |
|
|
173
174
|
| `ctx.tenantId` | Tenant ID from JWT; `'default'` for stdio or HTTP with auth off. |
|
|
174
175
|
|
|
175
176
|
---
|
|
@@ -187,11 +188,12 @@ Pixoo-specific error reasons declared on tools:
|
|
|
187
188
|
| `device_rejected` | `ServiceUnavailable` | Firmware returned non-zero `error_code` | — |
|
|
188
189
|
| `no_device_configured` | `InvalidParams` | Device tool called without `PIXOO_IP` | — |
|
|
189
190
|
| `asset_not_found` | `NotFound` | Image/sprite path or URL unreadable | — |
|
|
191
|
+
| `invalid_image` | `InvalidParams` | Image source or sprite path read but not decodable | — |
|
|
190
192
|
| `invalid_color` | `InvalidParams` | `resolveColor` throw — invalid color name or format | — |
|
|
191
193
|
| `unknown_icon` | `InvalidParams` | Icon name not in registry | — |
|
|
192
194
|
| `discovery_failed` | `ServiceUnavailable` | Divoom cloud unreachable | `true` |
|
|
193
195
|
|
|
194
|
-
`PixooService` and `src/renderer/` raise the device, configuration, and asset reasons themselves (a factory error carrying `data.reason`), so those entries carry `thrownBy: 'service'` — lint-only metadata that keeps `error-contract-unthrown` from reading them as dead. A reason the handler throws with `ctx.fail` stays unmarked
|
|
196
|
+
`PixooService` and `src/renderer/` raise the device, configuration, and asset reasons themselves (a factory error carrying `data.reason`), so those entries carry `thrownBy: 'service'` — lint-only metadata that keeps `error-contract-unthrown` from reading them as dead. A reason the handler throws with `ctx.fail` stays unmarked. Either way, the framework puts the entry's `recovery` on the wire when a failure carrying that `reason` arrives without a hint — a bare `ctx.fail('reason', message)` or a service throw with `data: { reason }` — as `data.recovery.hint`, mirrored into `content[]` unless the message already contains it. Pass an explicit `{ recovery: { hint } }` only to override it for one occurrence — a throw-site hint wins over the contract, so wording that holds for every occurrence of a reason lives in its `errors[]` entry, not at the throw. Every error envelope also carries `data.requestId`, and `content[]` closes with `(reason … · request <id>)`.
|
|
195
197
|
|
|
196
198
|
A contract's `retryable` reaches the wire only through `ctx.fail`; a service throw carries it only when the service writes `data.retryable` itself. `classifyDeviceFailure` (in `pixoo-service.ts`) is the one place a failed device call becomes a reason and a retryability — `PixooService` writes both on its push-path throws, and `pixoo_overlay_text` passes the retryability into `ctx.fail` so it overrides the contract default per occurrence.
|
|
197
199
|
|
|
@@ -211,7 +213,7 @@ errors: [
|
|
|
211
213
|
],
|
|
212
214
|
|
|
213
215
|
// in the handler
|
|
214
|
-
throw ctx.fail('invalid_color', `Invalid color "${input.color}"
|
|
216
|
+
throw ctx.fail('invalid_color', `Invalid color "${input.color}"`);
|
|
215
217
|
```
|
|
216
218
|
|
|
217
219
|
Baseline codes (`InternalError`, `ServiceUnavailable`, `Timeout`, `ValidationError`, `SerializationError`, `RequestCancelled`) bubble freely and don't need declaring. A tool argument that fails the input schema reaches the client as `InvalidParams` (-32602) with `structuredContent.error` — assert that code, not `ValidationError`, in tests.
|
|
@@ -234,11 +236,14 @@ src/
|
|
|
234
236
|
text-engine.ts # Gradient ramp + shadow + outline text engine, overflow handling
|
|
235
237
|
scene-renderer.ts # Element vocabulary, layout resolver, frame rendering
|
|
236
238
|
keyframes.ts # Keyframe interpolation + animation preset compiler
|
|
239
|
+
finish.ts # Palette finish (quantize + dither) for one frame, or frames sharing one palette
|
|
237
240
|
preview.ts # PNG/contact-sheet/GIF encoding
|
|
238
|
-
remote-image.ts # https image fetch
|
|
241
|
+
remote-image.ts # https image fetch into memory for the toolkit loader; stops on ctx.signal
|
|
239
242
|
mcp-server/
|
|
240
243
|
tools/
|
|
244
|
+
asset-source-schema.ts # Shared image `source` / sprite `path` schemas: absolute path or URL only
|
|
241
245
|
device-push.ts # Shared post-render push: preview kept on failure, visibility notice
|
|
246
|
+
finish-schema.ts # Shared `finish` input schema (colors | palette, plus dither)
|
|
242
247
|
tools/definitions/
|
|
243
248
|
pixoo-display-text.tool.ts
|
|
244
249
|
pixoo-compose-scene.tool.ts
|
|
@@ -254,6 +259,8 @@ src/
|
|
|
254
259
|
pixoo-design-guide.resource.ts
|
|
255
260
|
tests/
|
|
256
261
|
index.session-mode.test.ts # Boots the entry point over HTTP, pins the declared session mode
|
|
262
|
+
config/ # Server config parsed from env vars
|
|
263
|
+
helpers/ # Shared fixtures and assertions (image sources, canvas ink, device failures)
|
|
257
264
|
renderer/ # Pure renderer unit tests (no device)
|
|
258
265
|
resources/ # Resource handler tests
|
|
259
266
|
services/pixoo/ # PixooService tests with a fake client
|
|
@@ -375,7 +382,7 @@ Each per-version file opens with YAML frontmatter: `summary` (required, ≤350 c
|
|
|
375
382
|
## Checklist
|
|
376
383
|
|
|
377
384
|
- [ ] Zod schemas: all fields have `.describe()`, only JSON-Schema-serializable types (no `z.custom()`, `z.date()`, `z.transform()`, `z.bigint()`, `z.symbol()`, `z.void()`, `z.map()`, `z.set()`, `z.function()`, `z.nan()`)
|
|
378
|
-
- [ ] Tool inputs are
|
|
385
|
+
- [ ] Tool inputs are closed at every level — the framework closes the root `z.object`, and every nested input object is a `z.strictObject`, so an undeclared key fails by its full path (`Unknown key style.colour. style accepts: …`) instead of being dropped. A tool with nested input objects gets a schema test that walks its `input` and fails on any open object below the root (`tests/helpers/zod-object-nodes.ts`). Use `z.looseObject()` / `.catchall()` only where an open object is genuinely required
|
|
379
386
|
- [ ] JSDoc `@fileoverview` + `@module` on every file
|
|
380
387
|
- [ ] `ctx.log` for logging, `ctx.state` for storage
|
|
381
388
|
- [ ] Handlers throw on failure — error factories or plain `Error`, no try/catch
|
package/Dockerfile
CHANGED
|
@@ -29,80 +29,97 @@ RUN bun run build
|
|
|
29
29
|
|
|
30
30
|
|
|
31
31
|
# ==============================================================================
|
|
32
|
-
# Production Stage
|
|
32
|
+
# Production Dependencies Stage
|
|
33
33
|
#
|
|
34
|
-
#
|
|
35
|
-
#
|
|
36
|
-
#
|
|
34
|
+
# Installs the production dependency tree for the target platform. Every step
|
|
35
|
+
# here can run JavaScript — bunfig.toml's security scanner runs as a Bun
|
|
36
|
+
# program, and so does the OTel script — so the stage runs on $BUILDPLATFORM
|
|
37
|
+
# and cross-installs with `--os`/`--cpu`, which pick each platform-specific
|
|
38
|
+
# optional dependency (sharp's native `@img/*` bindings) for the target. Only
|
|
39
|
+
# `node_modules` leaves this stage.
|
|
40
|
+
#
|
|
41
|
+
# A clean image rather than `FROM build`: the build stage's node_modules holds
|
|
42
|
+
# devDependencies.
|
|
37
43
|
# ==============================================================================
|
|
38
|
-
FROM oven/bun:1.4.2
|
|
44
|
+
FROM --platform=$BUILDPLATFORM oven/bun:1.4.2 AS deps
|
|
39
45
|
|
|
40
46
|
WORKDIR /usr/src/app
|
|
41
47
|
|
|
42
|
-
# Set the environment to production for performance and to ensure only
|
|
43
|
-
# production dependencies are installed.
|
|
44
|
-
ENV NODE_ENV=production
|
|
45
|
-
|
|
46
|
-
# OCI image metadata (https://github.com/opencontainers/image-spec/blob/main/annotations.md)
|
|
47
|
-
ARG APP_VERSION
|
|
48
|
-
LABEL org.opencontainers.image.title="pixoo-mcp-server"
|
|
49
|
-
LABEL org.opencontainers.image.description="Render and push styled pixel art, text, dashboards, and animations to Divoom Pixoo LED displays."
|
|
50
|
-
LABEL org.opencontainers.image.licenses="Apache-2.0"
|
|
51
|
-
LABEL org.opencontainers.image.version="${APP_VERSION}"
|
|
52
|
-
LABEL org.opencontainers.image.source="https://github.com/cyanheads/pixoo-mcp-server"
|
|
53
|
-
|
|
54
48
|
# Copy dependency manifests. `bunfig.toml` rides along so every install below
|
|
55
49
|
# passes its release-age gate and security scanner, as a local install does.
|
|
56
50
|
COPY package.json bun.lock bunfig.toml ./
|
|
57
51
|
|
|
58
52
|
# The scanner bunfig.toml names is a devDependency, and Bun installs a missing
|
|
59
53
|
# scanner through the same production-filtered install, which omits it and
|
|
60
|
-
# aborts. Seed it from the build stage's full install instead. Remove this line
|
|
61
|
-
#
|
|
54
|
+
# aborts. Seed it from the build stage's full install instead. Remove this line,
|
|
55
|
+
# and the `rm` at the end of this stage, if bunfig.toml stops naming a scanner.
|
|
62
56
|
COPY --from=build /usr/src/app/node_modules/@socketsecurity/bun-security-scanner ./node_modules/@socketsecurity/bun-security-scanner
|
|
63
57
|
|
|
58
|
+
# Docker names the target architecture `amd64`/`arm64`; Bun's `--cpu` takes
|
|
59
|
+
# `x64`/`arm64`. Mapped once here, read by both installs below. `oven/bun`
|
|
60
|
+
# publishes only these two architectures, so any other target fails here.
|
|
61
|
+
ARG TARGETOS
|
|
62
|
+
ARG TARGETARCH
|
|
63
|
+
RUN case "$TARGETARCH" in \
|
|
64
|
+
amd64) echo x64 ;; \
|
|
65
|
+
arm64) echo arm64 ;; \
|
|
66
|
+
*) echo "Unsupported TARGETARCH '$TARGETARCH': expected amd64 or arm64" >&2; exit 1 ;; \
|
|
67
|
+
esac > .bun-cpu
|
|
68
|
+
|
|
64
69
|
# Install only production dependencies, ignoring any lifecycle scripts (like 'prepare')
|
|
65
70
|
# that are not needed in the final production image.
|
|
66
71
|
# `--omit=peer` drops the framework's optional peer tiers (test runner, service
|
|
67
72
|
# SDKs, parsers) that Bun would otherwise auto-install. Anything this server
|
|
68
73
|
# actually imports belongs in its own `dependencies`, so nothing needed at
|
|
69
|
-
# runtime is lost.
|
|
70
|
-
# install re-resolves the graph and pulls every optional peer back in.
|
|
74
|
+
# runtime is lost.
|
|
71
75
|
RUN --mount=type=cache,target=/root/.bun/install/cache \
|
|
72
|
-
bun install --production --omit=peer --frozen-lockfile --ignore-scripts
|
|
76
|
+
bun install --production --omit=peer --frozen-lockfile --ignore-scripts \
|
|
77
|
+
--os="$TARGETOS" --cpu="$(cat .bun-cpu)"
|
|
73
78
|
|
|
74
79
|
# Conditionally install OpenTelemetry optional peer dependencies (Tier 3).
|
|
75
80
|
# Installed by default. Omit them for a leaner image at build time
|
|
76
81
|
# with: docker build --build-arg OTEL_ENABLED=false
|
|
77
|
-
#
|
|
78
|
-
# `peerDependencies
|
|
79
|
-
|
|
82
|
+
# The script reads the list and each range from the installed framework's
|
|
83
|
+
# `peerDependencies` and passes the target flags on to its `bun install`.
|
|
84
|
+
COPY scripts/install-otel.ts ./scripts/
|
|
80
85
|
ARG OTEL_ENABLED=true
|
|
81
86
|
RUN --mount=type=cache,target=/root/.bun/install/cache \
|
|
82
87
|
if [ "$OTEL_ENABLED" = "true" ]; then \
|
|
83
|
-
|
|
84
|
-
const { peerDependencies: peers } = await Bun.file("node_modules/@cyanheads/mcp-ts-core/package.json").json(); \
|
|
85
|
-
const names = process.argv.slice(1); \
|
|
86
|
-
const missing = names.filter((name) => !peers?.[name]); \
|
|
87
|
-
if (missing.length > 0) throw new Error(`no peerDependencies range for ${missing.join(", ")}`); \
|
|
88
|
-
console.log(names.map((name) => `${name}@${peers[name]}`).join(" ")); \
|
|
89
|
-
' \
|
|
90
|
-
@hono/otel \
|
|
91
|
-
@opentelemetry/api-logs \
|
|
92
|
-
@opentelemetry/exporter-logs-otlp-http \
|
|
93
|
-
@opentelemetry/exporter-metrics-otlp-http \
|
|
94
|
-
@opentelemetry/exporter-trace-otlp-http \
|
|
95
|
-
@opentelemetry/instrumentation-http \
|
|
96
|
-
@opentelemetry/instrumentation-pino \
|
|
97
|
-
@opentelemetry/resources \
|
|
98
|
-
@opentelemetry/sdk-logs \
|
|
99
|
-
@opentelemetry/sdk-metrics \
|
|
100
|
-
@opentelemetry/sdk-node \
|
|
101
|
-
@opentelemetry/sdk-trace-node \
|
|
102
|
-
@opentelemetry/semantic-conventions) \
|
|
103
|
-
&& bun add --omit=dev --omit=peer --ignore-scripts $specs; \
|
|
88
|
+
bun scripts/install-otel.ts --os="$TARGETOS" --cpu="$(cat .bun-cpu)"; \
|
|
104
89
|
fi
|
|
105
90
|
|
|
91
|
+
# The seeded scanner served only the installs above; keep it out of the image.
|
|
92
|
+
RUN rm -rf node_modules/@socketsecurity/bun-security-scanner
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
# ==============================================================================
|
|
96
|
+
# Production Stage
|
|
97
|
+
#
|
|
98
|
+
# This stage creates a minimal, optimized, and secure image for running the
|
|
99
|
+
# application. It uses a slim base image and only includes production
|
|
100
|
+
# dependencies and build artifacts. Its only Bun invocations are HEALTHCHECK
|
|
101
|
+
# and CMD, which run on the real target at container start.
|
|
102
|
+
# ==============================================================================
|
|
103
|
+
FROM oven/bun:1.4.2-slim AS production
|
|
104
|
+
|
|
105
|
+
WORKDIR /usr/src/app
|
|
106
|
+
|
|
107
|
+
# Set the environment to production for performance.
|
|
108
|
+
ENV NODE_ENV=production
|
|
109
|
+
|
|
110
|
+
# OCI image metadata (https://github.com/opencontainers/image-spec/blob/main/annotations.md)
|
|
111
|
+
ARG APP_VERSION
|
|
112
|
+
LABEL org.opencontainers.image.title="pixoo-mcp-server"
|
|
113
|
+
LABEL org.opencontainers.image.description="Render and push styled pixel art, text, dashboards, and animations to Divoom Pixoo LED displays."
|
|
114
|
+
LABEL org.opencontainers.image.licenses="Apache-2.0"
|
|
115
|
+
LABEL org.opencontainers.image.version="${APP_VERSION}"
|
|
116
|
+
LABEL org.opencontainers.image.source="https://github.com/cyanheads/pixoo-mcp-server"
|
|
117
|
+
|
|
118
|
+
# The manifest comes from the build context: the deps stage's copy was rewritten
|
|
119
|
+
# by the OTel install, and the runtime reads only its name, version, and type.
|
|
120
|
+
COPY package.json ./
|
|
121
|
+
COPY --from=deps /usr/src/app/node_modules ./node_modules
|
|
122
|
+
|
|
106
123
|
# Copy the compiled application code from the build stage
|
|
107
124
|
COPY --from=build /usr/src/app/dist ./dist
|
|
108
125
|
|
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
<div align="center">
|
|
9
9
|
|
|
10
|
-
[](./CHANGELOG.md) [](./LICENSE) [](https://github.com/users/cyanheads/packages/container/package/pixoo-mcp-server) [](https://modelcontextprotocol.io/) [](https://www.npmjs.com/package/@cyanheads/pixoo-mcp-server) [](https://www.typescriptlang.org/) [](https://bun.sh/)
|
|
11
11
|
|
|
12
12
|
</div>
|
|
13
13
|
|
|
@@ -31,7 +31,7 @@ Divoom Pixoo LED matrix displays on the local network, with the Pixoo-64 as the
|
|
|
31
31
|
|:---|:---|
|
|
32
32
|
| `pixoo_display_text` | Render styled text with themes, gradients, shadows, and auto-fit, static or animated, and push it |
|
|
33
33
|
| `pixoo_compose_scene` | Compose layered scenes of text, icons, widgets, shapes, bitmaps, images, and sprites, static or animated |
|
|
34
|
-
| `pixoo_push_image` | Resize a local or https image to the LED grid and push it |
|
|
34
|
+
| `pixoo_push_image` | Resize a local or https image to the LED grid and push it, an animated GIF or WebP as an animation |
|
|
35
35
|
| `pixoo_overlay_text` | Set or clear a device-rendered scrolling text overlay |
|
|
36
36
|
| `pixoo_control_device` | Read or change brightness, screen state, channel, or clock face |
|
|
37
37
|
| `pixoo_discover_devices` | Find Pixoo devices and their LAN IPs through Divoom's cloud discovery |
|
|
@@ -53,23 +53,27 @@ Tools cover the same ground for tool-only clients: `pixoo_control_device` reads
|
|
|
53
53
|
### `pixoo_display_text` <sub>tool</sub>
|
|
54
54
|
|
|
55
55
|
- `text` as a string or an array of lines; `theme` (`midnight`, `ember`, `claude`, `ice`, `neon`, `forest`, `mono`) sets the background and default palette. `style` takes a `palette` ramp (`ember`, `ice`, `neon`, `fire`, `lavender`, `claude`, `mono`) or a custom `{ from, to }`, plus `shadow`, `outline`, and `scale` 1–8; `position` is semantic or in pixels, and `align` lines up multi-line text
|
|
56
|
-
- `
|
|
56
|
+
- `font`: `standard` (5×7) and `compact` (3×5) draw printable ASCII plus `° ← ↑ → ↓ ▲ ▼ ♥ · …`, and any other character (`€`, `’`, a newline inside one string) as `?`, named with its code point and line index in the response `notice`; `numerals` is an 11×18 digit face for clocks and readouts that draws 0–9, space, and `: . - + / % ° ?`, and text holding any other character fails validation, naming those characters
|
|
57
|
+
- `layout[]` reports every fit decision as an `action` (`shrunk-to-compact` when the text fits only in the compact font, `scrolling` when the returned frames scroll, `none` otherwise), the `font` used, and whether each line's box `fits` on the panel; a single line too wide for the panel starts at x 0, cut at the right edge when it does not scroll. Single-line text falls back from the standard to the compact font unless `font` is set — never to `numerals` — and text still too wide only scrolls under `effect: "auto"` or `"scroll"`
|
|
57
58
|
- `effect`: `scroll` makes one pass in up to 40 frames, `auto` scrolls only on overflow, and `float` and `pulse` loop over 20 frames; `frames` reports the count, and the animation pushes as one device animation
|
|
58
59
|
|
|
59
60
|
---
|
|
60
61
|
|
|
61
62
|
### `pixoo_compose_scene` <sub>tool</sub>
|
|
62
63
|
|
|
63
|
-
- Up to 50 `elements` drawn back-to-front: `text
|
|
64
|
-
-
|
|
65
|
-
-
|
|
64
|
+
- Up to 50 `elements` drawn back-to-front: `text` (in the same three fonts as `pixoo_display_text`), `icon`, `rect`, `circle`, `line`, `progress`, `sparkline`, `bitmap`, `pixels`, `image` (absolute path or https URL, with the same `finish` as `pixoo_push_image`), `sprite` (absolute path). The `background` is a solid color, a `v` / `h` / `r` gradient, or a `theme`
|
|
65
|
+
- Every element takes `opacity` (each pixel lands at its own alpha × `opacity`, so soft edges fade evenly) and `blend`: `normal`, `add` (glows and light beams), `screen`, or `multiply`. `line` and outline `circle` take `strokeWidth` and `antialias`, and a `rect` border takes `strokeWidth`, growing inward; either field on a shape that draws no stroke fails validation, naming it
|
|
66
|
+
- Returns `layout[]`: each element's placed box — for a wide or anti-aliased stroke, every pixel it draws — and whether it `fits` on the panel. Elements are placed as given and never refit, so `action` is always `none`. An absolute `output` path saves the first frame as a PNG in place of the `PIXOO_OUTPUT_DIR` auto-save. Typed failures: `asset_not_found`, `invalid_image` (an image or sprite that was read but does not decode), `invalid_color`, `unknown_icon`, `invalid_output_path`
|
|
67
|
+
- Animation through per-element `effect` presets (`float`, `scroll-left`, `scroll-right`, `pulse`, `blink`, `twinkle`, `drift`, `fade-in`, `fade-out`) or raw `animate` keyframes over `dx`, `dy`, `opacity` (numbers or numeric strings), `visible` (`true`/`false`), and `color` (interpolated through RGB on any element with a `color`), each track holding at least one keyframe; `frames` 1–40, `speed` 10–2000 ms per frame (default 150). An element takes `effect` or `animate`, not both. An effect's `amplitude` sets the movement of `float`, `scroll-*`, and `drift`, and the 0–1 depth of the `pulse` and `twinkle` opacity dip
|
|
66
68
|
|
|
67
69
|
---
|
|
68
70
|
|
|
69
71
|
### `pixoo_push_image` <sub>tool</sub>
|
|
70
72
|
|
|
71
73
|
- `source` is an absolute local path or an https URL, with downloads capped at 10 MB; `fit` is `contain` (default), `cover`, or `fill`, and `kernel` is `nearest` (default, for pixel art), `lanczos3` (photos), or `mitchell`
|
|
72
|
-
-
|
|
74
|
+
- A source that decodes as an animated GIF or WebP, whatever its file name, pushes as an animation of up to 40 frames, sampled evenly from a longer source. It plays at the source's total duration over the pushed frame count (150 ms when the source records no delays), or at `speed` (10–2000 ms per frame); `frames`, `sourceFrames`, and `speed` report what was pushed
|
|
75
|
+
- `finish` reduces the image to a palette before the push: exactly one of `colors` (2–256, built from the image) or `palette` (1–256 hex or named colors), plus `dither` (`none`, `bayer4`, `floyd-steinberg`). Transparent pixels stay unlit, and an animation's `colors` palette is shared by every frame
|
|
76
|
+
- An unreadable path or URL fails as `asset_not_found`, a source that is read but does not decode (a text file, an HTML page, a truncated download) fails as `invalid_image`, and an unresolvable `finish` palette entry fails as `invalid_color`; the preview is the exact frame the device receives, or a grid of every frame for an animation
|
|
73
77
|
|
|
74
78
|
---
|
|
75
79
|
|
|
@@ -135,12 +139,12 @@ Pixoo-specific:
|
|
|
135
139
|
|
|
136
140
|
- All composition happens on the host in an RGBA canvas pipeline (`@cyanheads/pixoo-toolkit`); the device receives finished RGB frames
|
|
137
141
|
- Pushes switch the device to the custom channel and run one at a time, spaced by `PIXOO_PUSH_MIN_INTERVAL_MS` (default 1000) so rapid pushes don't freeze the device
|
|
138
|
-
- Animations cap at 40 frames, past which the device becomes unstable
|
|
142
|
+
- Animations cap at 40 frames, past which the device becomes unstable; `pixoo_push_image` samples a longer GIF or WebP down to 40
|
|
139
143
|
|
|
140
144
|
Agent-friendly output:
|
|
141
145
|
|
|
142
146
|
- Preview on every render: `pixoo_display_text`, `pixoo_compose_scene`, and `pixoo_push_image` return the frame as an 8× upscaled PNG image block, pushed or not, so `push: false` checks a design with no device attached. Animations preview as a grid of every frame, since GIF display varies across MCP clients; the GIF itself is saved to `PIXOO_OUTPUT_DIR` when set
|
|
143
|
-
- Layout transparency: `layout[]` reports every renderer decision (font fallback,
|
|
147
|
+
- Layout transparency: `layout[]` reports every renderer decision (font fallback, scrolling, and whether each box fits on the panel) so agents can refine a design
|
|
144
148
|
- Device truth: `pushed` reflects the device ACK, and the `deviceState` read back after a push comes with a notice naming the fix when the render won't be visible (screen off, brightness ≤ 10, off the custom channel)
|
|
145
149
|
- Renders survive failed pushes: the typed error (`device_unreachable`, `device_http_error`, `device_rejected`, `no_device_configured`) carries `outputFiles` pointing at the saved preview (the `PIXOO_OUTPUT_DIR` copy, or a temp file when that is unset)
|
|
146
150
|
|
|
@@ -248,7 +252,7 @@ cp .env.example .env
|
|
|
248
252
|
|:---|:---|:---|
|
|
249
253
|
| `PIXOO_IP` | Device IP on the local network. **Required** for pushes, overlays, and device control; discovery, design briefs, and `push: false` renders work without it. | — |
|
|
250
254
|
| `PIXOO_SIZE` | Display size in pixels: `16`, `32`, or `64`. | `64` |
|
|
251
|
-
| `PIXOO_OUTPUT_DIR` | Directory where render tools save preview PNG and GIF files. Unset, previews are returned only in the response. | — |
|
|
255
|
+
| `PIXOO_OUTPUT_DIR` | Directory where render tools save preview PNG and GIF files. A relative path resolves against the directory the server was launched from, so the saved paths it reports are absolute. Unset, previews are returned only in the response. | — |
|
|
252
256
|
| `PIXOO_PUSH_MIN_INTERVAL_MS` | Minimum gap between device pushes, in ms. | `1000` |
|
|
253
257
|
| `MCP_TRANSPORT_TYPE` | Transport: `stdio` or `http`. | `stdio` |
|
|
254
258
|
| `MCP_HTTP_PORT` | HTTP server port. | `3010` |
|
|
@@ -300,10 +304,10 @@ The Dockerfile defaults to HTTP transport, stateless session mode, and logs to `
|
|
|
300
304
|
|:---|:---|
|
|
301
305
|
| `src/index.ts` | `createApp()` entry point: registers tools and resources and initializes the Pixoo service. |
|
|
302
306
|
| `src/config/` | Server-specific environment variable parsing and validation with Zod. |
|
|
303
|
-
| `src/mcp-server/tools/` | Tool definitions (`*.tool.ts`)
|
|
307
|
+
| `src/mcp-server/tools/` | Tool definitions (`*.tool.ts`), the shared post-render push path, and the shared `finish` input schema. |
|
|
304
308
|
| `src/mcp-server/resources/` | Resource definitions (`*.resource.ts`). |
|
|
305
309
|
| `src/services/pixoo/` | `PixooService`: wraps `@cyanheads/pixoo-toolkit` with push pacing, result mapping, and device state reads. |
|
|
306
|
-
| `src/renderer/` | Pure rendering pipeline with no device dependency: element renderers, styled-text engine, themes, icons, effect compiler, preview encoding, remote image fetch. |
|
|
310
|
+
| `src/renderer/` | Pure rendering pipeline with no device dependency: element renderers, styled-text engine, themes, icons, effect compiler, palette finishing, preview encoding, remote image fetch. |
|
|
307
311
|
| `tests/` | Unit and integration tests mirroring `src/`. |
|
|
308
312
|
|
|
309
313
|
## Development guide
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
---
|
|
2
|
+
summary: "Adopts @cyanheads/pixoo-toolkit 0.10.0: animated GIF and WebP push as animations, images take a palette finish, scene elements take blend modes and stroke widths, and text gains a numerals face. Undecodable images fail as invalid_image, and filled circles and JPEG/WebP images render differently."
|
|
3
|
+
breaking: false
|
|
4
|
+
security: false
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# 1.2.2 — 2026-09-26
|
|
8
|
+
|
|
9
|
+
## Added
|
|
10
|
+
|
|
11
|
+
- **`finish` on `pixoo_push_image` and scene `image` elements** reduces the image to a palette: `colors` (2–256) or `palette` (1–256 colors), plus `dither` (`none`, `bayer4`, `floyd-steinberg`) ([#27](https://github.com/cyanheads/pixoo-mcp-server/issues/27)). An animation's `colors` palette is shared across frames; a bad palette entry fails as `invalid_color`.
|
|
12
|
+
- **`blend` on every `pixoo_compose_scene` element** — `normal` (default), `add`, `screen`, or `multiply` ([#29](https://github.com/cyanheads/pixoo-mcp-server/issues/29)).
|
|
13
|
+
- **`strokeWidth` and `antialias` on `line` and outline `circle`; `strokeWidth` on a `rect` border**, growing inward (#29). Either on a shape that draws no stroke fails `-32602` naming it; a wide or anti-aliased stroke's layout box covers every pixel drawn.
|
|
14
|
+
- **`font: "numerals"`** on `pixoo_display_text` and scene `text` — an 11×18 digit face drawing 0–9, space, and `: . - + / % ° ?`; any other character fails `-32602` naming it, and auto-fit never picks it ([#30](https://github.com/cyanheads/pixoo-mcp-server/issues/30)).
|
|
15
|
+
- **`pixoo_push_image` `speed` input and `frames`, `sourceFrames`, `speed` output fields** for animated sources ([#28](https://github.com/cyanheads/pixoo-mcp-server/issues/28)).
|
|
16
|
+
- **`pixoo_output_dir` and `pixoo_push_min_interval_ms` options in `manifest.json`**, so a Claude Desktop bundle install can set `PIXOO_OUTPUT_DIR` and `PIXOO_PUSH_MIN_INTERVAL_MS`.
|
|
17
|
+
|
|
18
|
+
## Changed
|
|
19
|
+
|
|
20
|
+
- **An animated GIF or WebP given to `pixoo_push_image` pushes as an animation** instead of its first frame, whatever its file extension (#28). Up to 40 frames, sampled evenly from a longer source, play at the source's total duration over the pushed frame count.
|
|
21
|
+
- **Filled `circle` elements draw a rounder disc** (`dx² + dy² ≤ r² + r`, from the toolkit), so every filled circle's pixels change.
|
|
22
|
+
- **JPEG and WebP images decode at full resolution before resizing** (from the toolkit), so their pushed pixels change.
|
|
23
|
+
- **An image source that is read but does not decode fails as `-32602` `invalid_image`** naming the source and the decoder's reason, instead of an unclassified `-32603`, in `pixoo_push_image` and compose `image` and `sprite` elements ([#34](https://github.com/cyanheads/pixoo-mcp-server/issues/34)).
|
|
24
|
+
- **`asset_not_found` carries its tool's declared recovery hint, reworded, on both image tools**, and a sprite `path` given as a URL says sprite sheets take a local path ([#51](https://github.com/cyanheads/pixoo-mcp-server/issues/51)).
|
|
25
|
+
- **https image sources load from memory** — they render when the temp dir is unwritable, and an https SVG renders like the same local file instead of rasterizing at its intrinsic size ([#23](https://github.com/cyanheads/pixoo-mcp-server/issues/23)).
|
|
26
|
+
- **A partially transparent pixel under `opacity` below 100 fades at its own alpha × opacity**, so soft-edged images no longer brighten as they fade (#29).
|
|
27
|
+
- **`twinkle` renders the same frames for the same input**; its jitter comes from the frame index and element position ([#50](https://github.com/cyanheads/pixoo-mcp-server/issues/50)).
|
|
28
|
+
- **`pixoo_display_text` reports `fits: false` for text leaving the panel on any edge** ([#49](https://github.com/cyanheads/pixoo-mcp-server/issues/49)), and a scrolling line reports `box.x: 0`, where its frame draws it ([#55](https://github.com/cyanheads/pixoo-mcp-server/issues/55)).
|
|
29
|
+
- **`standard` and `compact` draw `° ← ↑ → ↓ ▲ ▼ ♥ · …` as glyphs** instead of `?` (#30).
|
|
30
|
+
- **`Object.prototype` names such as `constructor` fail as `invalid_color`** on every color input of `pixoo_compose_scene`, `pixoo_display_text`, and `pixoo_overlay_text`, which now rejects them before any device call ([#32](https://github.com/cyanheads/pixoo-mcp-server/issues/32)).
|
|
31
|
+
- **`pixoo_design_brief` and the design guide** describe dithering, anti-aliasing, the new glyphs and numerals face, a glow recipe, and the Pixoo-64's measured dark and mid-level response.
|
|
32
|
+
|
|
33
|
+
## Fixed
|
|
34
|
+
|
|
35
|
+
- **`pixoo_compose_scene` loads each distinct asset once per call** — elements sharing a sprite sheet, or an image source and placement, share one decode, and a URL is fetched once ([#52](https://github.com/cyanheads/pixoo-mcp-server/issues/52)).
|
|
36
|
+
- **Standard text at scale 2 is 14 px tall** in the design brief and guide, which said 10 px.
|
|
37
|
+
|
|
38
|
+
## Dependencies
|
|
39
|
+
|
|
40
|
+
- `@cyanheads/pixoo-toolkit` ^0.8.2 → ^0.10.0, exempt from the `bunfig.toml` release-age hold beside the framework
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
---
|
|
2
|
+
summary: "Stricter tool inputs: unknown nested keys, relative image paths, and effect with animate now fail -32602 instead of being dropped or resolved silently. layout[].action reports only what the frames do, standard and compact text names the characters it draws as ?, and errors carry the request ID."
|
|
3
|
+
breaking: true
|
|
4
|
+
security: false
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# 1.3.0 — 2026-09-26
|
|
8
|
+
|
|
9
|
+
## Added
|
|
10
|
+
|
|
11
|
+
- **Fallback-glyph `notice` on `pixoo_display_text` and `pixoo_compose_scene`** ([#57](https://github.com/cyanheads/pixoo-mcp-server/issues/57)). A `standard` or `compact` line, scene `text` element, or `progress` `label` holding a character those fonts draw as `?` names it by element and code point (`element 1 "×" (U+00D7)`); the render is unchanged.
|
|
12
|
+
|
|
13
|
+
## Changed
|
|
14
|
+
|
|
15
|
+
- **Image `source` and sprite `path` must be an absolute local path or a URL** ([#53](https://github.com/cyanheads/pixoo-mcp-server/issues/53)). A relative or `~` path, a `file://` URL, or `""` now fails `-32602` `invalid_arguments`: `source: Must be an absolute local path or an https URL, not a relative path, a ~ path, or a file:// URL.` (in a scene, `elements.<i>.source` / `elements.<i>.path`).
|
|
16
|
+
- **Every nested `pixoo_compose_scene` input object rejects unknown keys** ([#54](https://github.com/cyanheads/pixoo-mcp-server/issues/54)) — elements, `style` and its palette stop, `effect`, `animate`, rect `gradient`, `pixels` points, `background` — failing `-32602` by full path: `Unknown key elements.0.colour. elements.0 accepts: type, x, y, w, h, color, …`.
|
|
17
|
+
- **`animate` takes only the tracks an element drives** (#54): an unknown track fails as `Unknown key elements.0.animate.y. …`, and `bitmap`, `progress`, `image`, and `sprite` reject a `color` track.
|
|
18
|
+
- **A compose `background` object holds exactly one of `gradient` or `theme`** (#54). Both or neither fails `background: Set exactly one of gradient or theme.` instead of the gradient winning.
|
|
19
|
+
- **A scene element setting both `effect` and `animate` fails** ([#59](https://github.com/cyanheads/pixoo-mcp-server/issues/59)) with `elements.<i>.effect: Set either effect or animate on an element, not both: …`, instead of dropping the effect.
|
|
20
|
+
- **`pixoo_display_text` rejects unknown keys in `style`, its palette stop, `position`, and `background`** ([#60](https://github.com/cyanheads/pixoo-mcp-server/issues/60)): `Unknown key style.colour. style accepts: palette, shadow, outline, scale, color.`
|
|
21
|
+
- **`layout[].action` reports what the returned frames do** ([#56](https://github.com/cyanheads/pixoo-mcp-server/issues/56)). A `pixoo_display_text` line too wide to fit that does not scroll reports `none` with `fits: false`, not `scrolling`; the output schemas advertise `none`/`shrunk-to-compact`/`scrolling` (display text) and `none` (compose), and `element` is always a number.
|
|
22
|
+
- **Error results carry the request ID** (framework): `data.requestId`, with the text closing `(reason … · request <id>)`.
|
|
23
|
+
- **Input-validation rejections log at `notice`** instead of `error` (framework), tagged `mcp.error.severity: "notice"` on `mcp.errors.classified`.
|
|
24
|
+
|
|
25
|
+
## Fixed
|
|
26
|
+
|
|
27
|
+
- **A relative `PIXOO_OUTPUT_DIR` reports absolute `outputFiles`** ([#61](https://github.com/cyanheads/pixoo-mcp-server/issues/61)); it resolves against the launch directory when the config is read, so files land where they did.
|
|
28
|
+
- **Image URL schemes are read case-insensitively** (#53), so an `HTTPS://…` source is fetched rather than read as a local path.
|
|
29
|
+
- **`pixoo_discover_devices` declares the `discovery_failed` recovery hint callers receive**; the wording on the wire is unchanged.
|
|
30
|
+
- **Multi-arch Docker builds no longer run Bun under emulation**: production dependencies cross-install in a build-platform `deps` stage, and `lint:packaging` fails a target-platform stage that runs JavaScript.
|
|
31
|
+
|
|
32
|
+
## Dependencies
|
|
33
|
+
|
|
34
|
+
- `@cyanheads/mcp-ts-core` `^0.13.9` → `^0.13.10`
|
|
35
|
+
- `hono` `4.13.8` → `4.13.9` (transitive, via the framework)
|
|
@@ -6,7 +6,7 @@ import { z } from '@cyanheads/mcp-ts-core';
|
|
|
6
6
|
declare const ServerConfigSchema: z.ZodObject<{
|
|
7
7
|
pixooIp: z.ZodOptional<z.ZodString>;
|
|
8
8
|
pixooSize: z.ZodDefault<z.ZodCoercedNumber<unknown> & z.ZodType<16 | 32 | 64, unknown, z.core.$ZodTypeInternals<16 | 32 | 64, unknown>>>;
|
|
9
|
-
pixooOutputDir: z.ZodOptional<z.ZodString
|
|
9
|
+
pixooOutputDir: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>;
|
|
10
10
|
pixooPushMinIntervalMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
|
|
11
11
|
}, z.core.$strip>;
|
|
12
12
|
export type ServerConfig = z.infer<typeof ServerConfigSchema>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server-config.d.ts","sourceRoot":"","sources":["../../src/config/server-config.ts"],"names":[],"mappings":"AAAA;;;GAGG;
|
|
1
|
+
{"version":3,"file":"server-config.d.ts","sourceRoot":"","sources":["../../src/config/server-config.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAE,CAAC,EAAE,MAAM,wBAAwB,CAAC;AAG3C,QAAA,MAAM,kBAAkB;;;;;iBAsBtB,CAAC;AAEH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAI9D,wBAAgB,eAAe,IAAI,YAAY,CAQ9C;AAED,yCAAyC;AACzC,wBAAgB,iBAAiB,IAAI,IAAI,CAExC"}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* @fileoverview Server-specific environment variable configuration for pixoo-mcp-server.
|
|
3
3
|
* @module config/server-config
|
|
4
4
|
*/
|
|
5
|
+
import * as path from 'node:path';
|
|
5
6
|
import { z } from '@cyanheads/mcp-ts-core';
|
|
6
7
|
import { parseEnvConfig } from '@cyanheads/mcp-ts-core/config';
|
|
7
8
|
const ServerConfigSchema = z.object({
|
|
@@ -13,7 +14,11 @@ const ServerConfigSchema = z.object({
|
|
|
13
14
|
})
|
|
14
15
|
.default(64)
|
|
15
16
|
.describe('Display size in pixels (16, 32, or 64).'),
|
|
16
|
-
pixooOutputDir: z
|
|
17
|
+
pixooOutputDir: z
|
|
18
|
+
.string()
|
|
19
|
+
.transform((dir) => path.resolve(dir))
|
|
20
|
+
.optional()
|
|
21
|
+
.describe('Auto-save directory for preview PNG/GIF files. A relative path resolves against the working directory the server was launched from, so every saved path it reports is absolute.'),
|
|
17
22
|
pixooPushMinIntervalMs: z.coerce
|
|
18
23
|
.number()
|
|
19
24
|
.int()
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server-config.js","sourceRoot":"","sources":["../../src/config/server-config.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,wBAAwB,CAAC;AAC3C,OAAO,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAE/D,MAAM,kBAAkB,GAAG,CAAC,CAAC,MAAM,CAAC;IAClC,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,iCAAiC,CAAC;IAC1E,SAAS,EAAE,CAAC,CAAC,MAAM;SAChB,MAAM,EAAE;SACR,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,EAAE;QAC/C,OAAO,EAAE,uBAAuB;KACjC,CAAC;SACD,OAAO,CAAC,EAAE,CAAC;SACX,QAAQ,CAAC,yCAAyC,CAAC;IACtD,cAAc,EAAE,CAAC,
|
|
1
|
+
{"version":3,"file":"server-config.js","sourceRoot":"","sources":["../../src/config/server-config.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAClC,OAAO,EAAE,CAAC,EAAE,MAAM,wBAAwB,CAAC;AAC3C,OAAO,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAE/D,MAAM,kBAAkB,GAAG,CAAC,CAAC,MAAM,CAAC;IAClC,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,iCAAiC,CAAC;IAC1E,SAAS,EAAE,CAAC,CAAC,MAAM;SAChB,MAAM,EAAE;SACR,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,EAAE;QAC/C,OAAO,EAAE,uBAAuB;KACjC,CAAC;SACD,OAAO,CAAC,EAAE,CAAC;SACX,QAAQ,CAAC,yCAAyC,CAAC;IACtD,cAAc,EAAE,CAAC;SACd,MAAM,EAAE;SACR,SAAS,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;SACrC,QAAQ,EAAE;SACV,QAAQ,CACP,iLAAiL,CAClL;IACH,sBAAsB,EAAE,CAAC,CAAC,MAAM;SAC7B,MAAM,EAAE;SACR,GAAG,EAAE;SACL,GAAG,CAAC,CAAC,CAAC;SACN,OAAO,CAAC,IAAI,CAAC;SACb,QAAQ,CAAC,qDAAqD,CAAC;CACnE,CAAC,CAAC;AAIH,IAAI,OAAiC,CAAC;AAEtC,MAAM,UAAU,eAAe;IAC7B,OAAO,KAAK,cAAc,CAAC,kBAAkB,EAAE;QAC7C,OAAO,EAAE,UAAU;QACnB,SAAS,EAAE,YAAY;QACvB,cAAc,EAAE,kBAAkB;QAClC,sBAAsB,EAAE,4BAA4B;KACrD,CAAC,CAAC;IACH,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,yCAAyC;AACzC,MAAM,UAAU,iBAAiB;IAC/B,OAAO,GAAG,SAAS,CAAC;AACtB,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pixoo-design-guide.resource.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/resources/definitions/pixoo-design-guide.resource.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAY,CAAC,EAAE,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"pixoo-design-guide.resource.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/resources/definitions/pixoo-design-guide.resource.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAY,CAAC,EAAE,MAAM,wBAAwB,CAAC;AA2GrD,eAAO,MAAM,wBAAwB,2GAyBnC,CAAC"}
|