@cyanheads/pixoo-mcp-server 1.1.1 → 1.1.3

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 (35) hide show
  1. package/AGENTS.md +93 -36
  2. package/CLAUDE.md +93 -36
  3. package/README.md +100 -71
  4. package/changelog/1.1.x/1.1.2.md +36 -0
  5. package/changelog/1.1.x/1.1.3.md +30 -0
  6. package/changelog/template.md +9 -26
  7. package/dist/index.js +3 -3
  8. package/dist/index.js.map +1 -1
  9. package/dist/mcp-server/tools/definitions/pixoo-compose-scene.tool.d.ts +5 -1
  10. package/dist/mcp-server/tools/definitions/pixoo-compose-scene.tool.d.ts.map +1 -1
  11. package/dist/mcp-server/tools/definitions/pixoo-compose-scene.tool.js +9 -6
  12. package/dist/mcp-server/tools/definitions/pixoo-compose-scene.tool.js.map +1 -1
  13. package/dist/mcp-server/tools/definitions/pixoo-control-device.tool.d.ts +1 -11
  14. package/dist/mcp-server/tools/definitions/pixoo-control-device.tool.d.ts.map +1 -1
  15. package/dist/mcp-server/tools/definitions/pixoo-control-device.tool.js +1 -13
  16. package/dist/mcp-server/tools/definitions/pixoo-control-device.tool.js.map +1 -1
  17. package/dist/mcp-server/tools/definitions/pixoo-design-brief.tool.js +1 -1
  18. package/dist/mcp-server/tools/definitions/pixoo-discover-devices.tool.d.ts +1 -0
  19. package/dist/mcp-server/tools/definitions/pixoo-discover-devices.tool.d.ts.map +1 -1
  20. package/dist/mcp-server/tools/definitions/pixoo-discover-devices.tool.js +1 -0
  21. package/dist/mcp-server/tools/definitions/pixoo-discover-devices.tool.js.map +1 -1
  22. package/dist/mcp-server/tools/definitions/pixoo-display-text.tool.d.ts +4 -1
  23. package/dist/mcp-server/tools/definitions/pixoo-display-text.tool.d.ts.map +1 -1
  24. package/dist/mcp-server/tools/definitions/pixoo-display-text.tool.js +5 -2
  25. package/dist/mcp-server/tools/definitions/pixoo-display-text.tool.js.map +1 -1
  26. package/dist/mcp-server/tools/definitions/pixoo-overlay-text.tool.d.ts +2 -1
  27. package/dist/mcp-server/tools/definitions/pixoo-overlay-text.tool.d.ts.map +1 -1
  28. package/dist/mcp-server/tools/definitions/pixoo-overlay-text.tool.js +9 -6
  29. package/dist/mcp-server/tools/definitions/pixoo-overlay-text.tool.js.map +1 -1
  30. package/dist/mcp-server/tools/definitions/pixoo-push-image.tool.d.ts +3 -0
  31. package/dist/mcp-server/tools/definitions/pixoo-push-image.tool.d.ts.map +1 -1
  32. package/dist/mcp-server/tools/definitions/pixoo-push-image.tool.js +4 -1
  33. package/dist/mcp-server/tools/definitions/pixoo-push-image.tool.js.map +1 -1
  34. package/package.json +11 -10
  35. package/server.json +3 -3
package/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
  <div align="center">
9
9
 
10
- [![Version](https://img.shields.io/badge/Version-1.1.1-blue.svg?style=flat-square)](./CHANGELOG.md) [![License](https://img.shields.io/badge/License-Apache%202.0-orange.svg?style=flat-square)](./LICENSE) [![Docker](https://img.shields.io/badge/Docker-ghcr.io-2496ED?style=flat-square&logo=docker&logoColor=white)](https://github.com/users/cyanheads/packages/container/package/pixoo-mcp-server) [![MCP SDK](https://img.shields.io/badge/MCP%20SDK-^2.0.0-green.svg?style=flat-square)](https://modelcontextprotocol.io/) [![npm](https://img.shields.io/npm/v/@cyanheads/pixoo-mcp-server?style=flat-square&logo=npm&logoColor=white)](https://www.npmjs.com/package/@cyanheads/pixoo-mcp-server) [![TypeScript](https://img.shields.io/badge/TypeScript-^7.0.2-3178C6.svg?style=flat-square)](https://www.typescriptlang.org/) [![Bun](https://img.shields.io/badge/Bun-v1.4.0-blueviolet.svg?style=flat-square)](https://bun.sh/)
10
+ [![Version](https://img.shields.io/badge/Version-1.1.3-blue.svg?style=flat-square)](./CHANGELOG.md) [![License](https://img.shields.io/badge/License-Apache%202.0-orange.svg?style=flat-square)](./LICENSE) [![Docker](https://img.shields.io/badge/Docker-ghcr.io-2496ED?style=flat-square&logo=docker&logoColor=white)](https://github.com/users/cyanheads/packages/container/package/pixoo-mcp-server) [![MCP SDK](https://img.shields.io/badge/MCP%20SDK-^2.0.0-green.svg?style=flat-square)](https://modelcontextprotocol.io/) [![npm](https://img.shields.io/npm/v/@cyanheads/pixoo-mcp-server?style=flat-square&logo=npm&logoColor=white)](https://www.npmjs.com/package/@cyanheads/pixoo-mcp-server) [![TypeScript](https://img.shields.io/badge/TypeScript-^7.0.2-3178C6.svg?style=flat-square)](https://www.typescriptlang.org/) [![Bun](https://img.shields.io/badge/Bun-v1.4.0-blueviolet.svg?style=flat-square)](https://bun.sh/)
11
11
 
12
12
  </div>
13
13
 
@@ -21,9 +21,11 @@
21
21
 
22
22
  ---
23
23
 
24
- ## Tools
24
+ ## Overview
25
25
 
26
- Seven tools covering the full display pipeline — from quick styled text to full layered scene composition, device control, and initial setup:
26
+ Divoom Pixoo LED matrix displays (Pixoo-64 primary; 16 and 32 also supported) on the local network. Render and push styled text, layered scenes, dashboards, and animations, or control device state, from any MCP client. Runs as a stdio process or a local Streamable HTTP server.
27
+
28
+ ### Tools
27
29
 
28
30
  | Tool | Description |
29
31
  |:-----|:------------|
@@ -35,87 +37,118 @@ Seven tools covering the full display pipeline — from quick styled text to ful
35
37
  | `pixoo_discover_devices` | Find Pixoo devices on the local network via Divoom's cloud discovery endpoint. Run once during setup to find device IPs. |
36
38
  | `pixoo_design_brief` | Return craft guidance and live device context for a design topic. Covers legibility rules, palette discipline, layout zones, animation budget, and pre-filled next-tool suggestions. |
37
39
 
38
- ### `pixoo_display_text`
39
-
40
- The primary tool for text-only display. Covers the 80% case — styled text with quality defaults.
40
+ ### Resources
41
41
 
42
- - Named scene themes set background gradient and text palette in one parameter (`midnight`, `ember`, `claude`, `ice`, `neon`, `forest`, `mono`)
43
- - Style block: gradient palette ramps (`ember`, `ice`, `neon`, `fire`, `lavender`, `claude`, `mono`), drop shadow, 1px outline for legibility, integer scale multiplier for block-letter weight
44
- - Semantic positioning: `x: "center"`, `y: "bottom"` — no manual pixel math
45
- - Auto-fit overflow: tries 5×7 → 3×5 → scroll; every fit decision reported in `layout[]`
46
- - Returns the rendered frame as an image content block so you see it immediately
47
- - Optional brightness convenience parameter applied before push
42
+ | Resource | Description |
43
+ |:---|:---|
44
+ | `pixoo://device/status` | Live snapshot of the connected Pixoo display: reachable, channel, brightness, screen state, and display size |
45
+ | `pixoo://reference/themes` | Theme and palette registry with background gradients, default text palettes, accent colors, and swatch values |
46
+ | `pixoo://reference/icons` | Built-in icon names organized by category (weather, arrows, status, media) |
47
+ | `pixoo://reference/design-guide` | Long-form 64px craft guide: legibility floors, palette discipline, layout zones, animation budget, and known device behaviors |
48
48
 
49
- ---
49
+ All resource data is also reachable via tools. `pixoo_design_brief` surfaces the design guide content per topic; `pixoo_control_device` returns live device state equivalent to `pixoo://device/status`.
50
50
 
51
- ### `pixoo_compose_scene`
51
+ ## Capability reference
52
52
 
53
- Full scene composition with the complete element vocabulary.
53
+ ### `pixoo_display_text` <sub>tool</sub>
54
54
 
55
- - Layered elements rendered back-to-front: `text`, `icon`, `rect`, `circle`, `line`, `progress`, `sparkline`, `bitmap`, `pixels`, `image`, `sprite`
56
- - Named icons from the built-in registry (weather, arrows, status, media) or custom SVG path
57
- - Dashboard widgets: `progress` bar with gradient fill and optional label; `sparkline` mini chart (line or bar, auto-scaled)
58
- - Animation: named effect presets (`float`, `scroll-left`, `scroll-right`, `pulse`, `blink`, `twinkle`, `drift`, `fade-in`, `fade-out`) or raw keyframe arrays — 1–40 frames, configurable speed
59
- - Per-element opacity and `visible` flag; images at https URLs fetched server-side to a temp file
60
- - Returns a preview image (static: PNG; animated: labeled contact-sheet PNG + GIF saved to disk)
55
+ - Named scene themes set background gradient and default text palette in one parameter (`midnight`, `ember`, `claude`, `ice`, `neon`, `forest`, `mono`)
56
+ - Style block: palette ramps (`ember`, `ice`, `neon`, `fire`, `lavender`, `claude`, `mono`) or a custom gradient/flat color, optional drop shadow, 1px outline, integer scale 1–8
57
+ - Semantic positioning (`x: "center"`, `y: "bottom"`) or absolute pixel coordinates; multi-line text stacks vertically with configurable alignment
58
+ - Auto-fit overflow tries standard font, then compact, then scroll; every fit decision is reported in `layout[]` with an `action` (`shrunk-to-compact`, `scrolling`, `wrapped`, `truncated`, `clipped`)
59
+ - Optional `brightness` (0–100) applied before push — a failure is a warning via an enrichment notice, not a tool error
60
+ - Returns the rendered frame as an image content block; `outputFiles` is populated only when `PIXOO_OUTPUT_DIR` is configured
61
61
 
62
62
  ---
63
63
 
64
- ### `pixoo_push_image`
64
+ ### `pixoo_compose_scene` <sub>tool</sub>
65
+
66
+ - Up to 50 layered elements rendered back-to-front: `text`, `icon`, `rect`, `circle`, `line`, `progress`, `sparkline`, `bitmap`, `pixels`, `image`, `sprite`
67
+ - Background: solid color, gradient (vertical, horizontal, or radial), or named theme
68
+ - Animation via named effect presets (`float`, `scroll-left`, `scroll-right`, `pulse`, `blink`, `twinkle`, `drift`, `fade-in`, `fade-out`) or raw per-property keyframe arrays — 1–40 frames at 10–2000ms per frame (default 150ms)
69
+ - `image` and `sprite` elements accept an absolute local path or an https URL; a supplied `output` path must be absolute with no traversal segments
70
+ - Static scenes return a PNG preview; animations return a labeled contact-sheet PNG plus a saved GIF (GIF preview is inconsistent across MCP clients)
71
+ - Typed failures for `asset_not_found`, `invalid_color`, and `unknown_icon`, alongside the shared device-error reasons
72
+
73
+ ---
65
74
 
66
- Push any image to the display with control over the downsampling.
75
+ ### `pixoo_push_image` <sub>tool</sub>
67
76
 
68
- - Accepts absolute local paths and https URLs
77
+ - Accepts an absolute local file path or an https (not http) URL
69
78
  - Three fit modes: `contain` (letterbox), `cover` (crop to fill), `fill` (stretch)
70
- - Three resize kernels: `nearest` for pixel art, `lanczos3` for photos, `mitchell` for a balance
71
- - Returns the exact 64×64 result as an image block — you see what the display received
79
+ - Three resize kernels: `nearest` for pixel art (default), `lanczos3` for photos, `mitchell` for a balance
80
+ - Returns the exact resized result as an image content block before it is pushed
72
81
 
73
82
  ---
74
83
 
75
- ### `pixoo_overlay_text`
84
+ ### `pixoo_overlay_text` <sub>tool</sub>
76
85
 
77
- Device-native scrolling text overlay — persists across channel switches.
86
+ - `mode: "set"` adds or updates an overlay on one of 20 independent slots (`id` 0–19); `mode: "clear"` removes it
87
+ - 115 device-rendered font IDs (0–114); overlays persist across channel switches until explicitly cleared
88
+ - Configurable `x`/`y` (0–64), scroll `direction` (`left`/`right`), `speed` (0–100), and `align`; color is `#RRGGBB` hex only — named colors aren't supported here
89
+ - Device-rendered, not previewable — for styled, previewable text use `pixoo_display_text`
78
90
 
79
- - 115 device-rendered font IDs (0–114)
80
- - Up to 20 independent overlay slots (IDs 0–19)
81
- - Configurable scroll direction, speed, and alignment
82
- - Clears with `mode: "clear"` — overlays survive channel changes until explicitly removed
83
- - Not previewable (device-rendered); for styled previewable text use `pixoo_display_text`
91
+ ---
92
+
93
+ ### `pixoo_control_device` <sub>tool</sub>
94
+
95
+ - Call with no params to read state only; supply any of `brightness` (0–100), `screen` (`on`/`off`), `channel` (`faces`/`cloud`/`visualizer`/`custom`), or `clockFaceId` to apply changes before the read-back
96
+ - `applied` lists which requested settings succeeded; a failed setting is omitted from `applied` and reported via an enrichment notice instead of failing the call
97
+ - Always returns current `reachable`, `channel`, `brightness`, `screenOn`, and `clockId` (the latter three absent when the device is unreachable)
84
98
 
85
99
  ---
86
100
 
87
- ### `pixoo_design_brief`
101
+ ### `pixoo_discover_devices` <sub>tool</sub>
88
102
 
89
- The orientation tool. Run before authoring any scene to get grounded in 64px craft constraints.
103
+ - Queries Divoom's cloud discovery endpoint (`app.divoom-gz.com`) — requires internet access even for local device control
104
+ - Returns each device's name, numeric ID, and LAN IP to set as `PIXOO_IP`
105
+ - When `PIXOO_IP` is already configured, flags whether it matches a discovered device (`configuredIpFound`) and notes a mismatch
106
+ - `timeoutMs` configurable 1000–30000ms (default 5000ms)
107
+
108
+ ---
109
+
110
+ ### `pixoo_design_brief` <sub>tool</sub>
90
111
 
91
112
  - Six topics: `text`, `scene`, `dashboard`, `animation`, `pixel-art`, `troubleshooting`
92
- - Returns legibility floors, palette discipline, layout zones, animation budgets, and common pitfalls
93
- - Merges live device state (reachable, channel, brightness, screen) into the response
94
- - Pre-filled `nextToolSuggestions` with ready-to-use arguments based on current device state
113
+ - Returns markdown craft guidance (legibility floors, palette discipline, layout zones, animation budgets) plus a live `deviceContext` snapshot
114
+ - `nextToolSuggestions` are pre-filled with ready-to-use arguments tailored to the topic and current device state (e.g. suggests `pixoo_discover_devices` when the device is unreachable)
115
+ - Also returns `availableThemes` and `iconCategories` for direct use in other tools
95
116
 
96
117
  ---
97
118
 
98
- ## Resources
119
+ ### `pixoo://device/status` <sub>resource</sub>
99
120
 
100
- | Type | Name | Description |
101
- |:-----|:-----|:------------|
102
- | Resource | `pixoo://device/status` | Live snapshot of the connected Pixoo display: reachable, channel, brightness, screen state, and display size |
103
- | Resource | `pixoo://reference/themes` | Theme and palette registry with background gradients, default text palettes, accent colors, and swatch values |
104
- | Resource | `pixoo://reference/icons` | Built-in icon names organized by category (weather, arrows, status, media) |
105
- | Resource | `pixoo://reference/design-guide` | Long-form 64px craft guide: legibility floors, palette discipline, layout zones, animation budget, and known device behaviors |
121
+ - Live snapshot: `reachable`, `channel`, `brightness`, `screenOn`, `clockId`, `displaySize`, `configuredIp`
122
+ - No cache — every read reaches the device; degrades to `reachable: false` instead of erroring when the device is unreachable
123
+ - Equivalent to calling `pixoo_control_device` with no params
106
124
 
107
- All resource data is also reachable via tools. `pixoo_design_brief` surfaces the design guide content per topic; `pixoo_control_device` returns live device state equivalent to `pixoo://device/status`.
125
+ ---
108
126
 
109
- ## Features
127
+ ### `pixoo://reference/themes` <sub>resource</sub>
110
128
 
111
- Built on [`@cyanheads/mcp-ts-core`](https://www.npmjs.com/package/@cyanheads/mcp-ts-core):
129
+ - Every registered theme (background gradient or solid, default text palette, accent color, shadow flag) and every named palette (gradient stop pair)
130
+ - `themeNames` / `paletteNames` arrays for direct use in the `theme` / `palette` parameters
131
+ - Compile-time constants — cached for 24h
132
+
133
+ ---
112
134
 
113
- - Declarative tool and resource definitions — single file per primitive, framework handles registration and validation
114
- - Unified error handling — handlers throw, framework catches, classifies, and formats
115
- - Pluggable auth: `none`, `jwt`, `oauth`
116
- - Swappable storage backends: `in-memory`, `filesystem`, `Supabase`, `Cloudflare KV/R2/D1`
117
- - Structured logging with optional OpenTelemetry tracing
118
- - STDIO and Streamable HTTP transports, serving MCP protocol revision 2026-07-28 alongside the 2025 revisions
135
+ ### `pixoo://reference/icons` <sub>resource</sub>
136
+
137
+ - Every built-in icon name, its category, and its SVG `viewBox`, plus a `byCategory` grouping (weather, arrows, status, media)
138
+ - Use a `name` from this registry in `pixoo_compose_scene` icon elements
139
+ - Compile-time constants — cached for 24h
140
+
141
+ ---
142
+
143
+ ### `pixoo://reference/design-guide` <sub>resource</sub>
144
+
145
+ - Long-form markdown: legibility floors, palette discipline, layout zones (top/middle/bottom strip pixel ranges), animation budget, pixel art rules, and known device behaviors (e.g. channel must be `custom` to show pushed content)
146
+ - `text/markdown` mime type; compile-time constant, cached for 24h
147
+ - Same content `pixoo_design_brief` surfaces per topic — this resource is the complete reference in one document
148
+
149
+ ## Features
150
+
151
+ Built on [`@cyanheads/mcp-ts-core`](https://github.com/cyanheads/mcp-ts-core): stdio and Streamable HTTP transports, pluggable auth (`none` / `jwt` / `oauth`), swappable storage (`in-memory`, `filesystem`, `Supabase`, `Cloudflare KV/R2/D1`), structured logging with optional OpenTelemetry tracing.
119
152
 
120
153
  Pixoo-specific:
121
154
 
@@ -123,22 +156,18 @@ Pixoo-specific:
123
156
  - All composition happens in an RGBA canvas pipeline on the host (`@cyanheads/pixoo-toolkit`) — the device receives final RGB frames, never raw drawing commands
124
157
  - Styled text engine: gradient palette ramps, drop shadows, outlines, integer scale, semantic alignment — no manual pixel math or bitmap letterforms required
125
158
  - Push pacing: device commands serialized with a configurable minimum inter-push interval (default 1000ms) to prevent device freezes
126
- - Every `PixooResult` checked — `pushed: true` means the device acknowledged with `error_code: 0`, never "I tried"
127
- - Animation capped at 40 frames (device instability beyond this); contact-sheet PNG preview for animations (GIF inconsistent across MCP clients)
128
- - Local transports only — `sharp` image processing doesn't run on Cloudflare Workers
159
+ - Animation capped at 40 frames (device instability beyond this); contact-sheet PNG preview for animations (GIF preview is inconsistent across MCP clients)
129
160
 
130
161
  Agent-friendly output:
131
162
 
132
- - **Preview-as-content**: render tools return the upscaled (8×, 512px) output as an image content block — the calling model sees exactly what was drawn, before and after push
133
- - **Layout transparency**: every silent renderer decision (font fallback, truncation, scroll engaged, element clipped) reported in `layout[]` so agents can inspect and refine
134
- - **Device truth**: `pushed` reflects the device ACK; `deviceState` post-push flags visibility issues (screen off, brightness ≤ 10, wrong channel) as enrichment notices rather than failures
135
- - **Graceful degradation**: render succeeds and returns the preview even when the device is unreachable — the agent keeps its work
163
+ - Preview-as-content — render tools return the upscaled (8×, 512px) output as an image content block, so the calling model sees exactly what was drawn, before and after push
164
+ - Layout transparency — every silent renderer decision (font fallback, truncation, scroll engaged, element clipped) is reported in `layout[]` so agents can inspect and refine
165
+ - Device truth — `pushed` reflects the device ACK; `deviceState` after a push flags visibility issues (screen off, brightness ≤ 10, wrong channel) as enrichment notices rather than failures
166
+ - Graceful degradation — render succeeds and returns the preview even when the device is unreachable, so the agent keeps its work
136
167
 
137
168
  ## Getting started
138
169
 
139
- **Requirements:** A Divoom Pixoo display (Pixoo-64, Pixoo-32, or Pixoo-16) on the same local network as the server. Run `pixoo_discover_devices` to find its IP, then set `PIXOO_IP` in your server configuration.
140
-
141
- Add the following to your MCP client configuration file:
170
+ Add the following to your MCP client configuration file. Run `pixoo_discover_devices` to find your Pixoo's IP, then set `PIXOO_IP` below.
142
171
 
143
172
  ```json
144
173
  {
@@ -204,7 +233,7 @@ MCP_TRANSPORT_TYPE=http MCP_HTTP_PORT=3010 PIXOO_IP=192.168.1.50 bun run start:h
204
233
 
205
234
  ### Prerequisites
206
235
 
207
- - [Bun v1.3.2](https://bun.sh/) or higher (or Node.js v24+).
236
+ - [Bun v1.4.0](https://bun.sh/) or higher (or Node.js v24+).
208
237
  - A Divoom Pixoo LED matrix display on the local network (Pixoo-64, Pixoo-32, or Pixoo-16). Discovery tools and pure-render tools (`push: false`) work without a configured device.
209
238
 
210
239
  ### Installation
@@ -240,13 +269,13 @@ All configuration is validated at startup via Zod schemas in `src/config/server-
240
269
 
241
270
  | Variable | Description | Default |
242
271
  |:---------|:------------|:--------|
243
- | `PIXOO_IP` | Device IP address on the local network. Required for device tools (`pixoo_display_text`, `pixoo_compose_scene`, `pixoo_push_image`, `pixoo_overlay_text`, `pixoo_control_device`). Discovery and pure-render (`push: false`) work without it. | — |
272
+ | `PIXOO_IP` | Device IP address on the local network. **Required for device tools** (`pixoo_display_text`, `pixoo_compose_scene`, `pixoo_push_image`, `pixoo_overlay_text`, `pixoo_control_device`). Discovery and pure-render (`push: false`) work without it. | — |
244
273
  | `PIXOO_SIZE` | Display size in pixels: `16`, `32`, or `64`. | `64` |
245
274
  | `PIXOO_OUTPUT_DIR` | Directory for auto-saving preview PNG and GIF files. When unset, previews are returned in-response only. | — |
246
275
  | `PIXOO_PUSH_MIN_INTERVAL_MS` | Minimum interval between device pushes in milliseconds. Prevents device freeze from rapid-fire commands. | `1000` |
247
276
  | `MCP_TRANSPORT_TYPE` | Transport: `stdio` or `http`. | `stdio` |
248
277
  | `MCP_HTTP_PORT` | HTTP server port. | `3010` |
249
- | `MCP_SESSION_MODE` | HTTP session handling: `stateful`, `stateless`, or `auto`. Shipped as `stateless` in `.env.example` and the Dockerfile — no tool requests input mid-call. | `auto` (resolves to `stateful`) |
278
+ | `MCP_SESSION_MODE` | HTTP session handling: `stateful`, `stateless`, or `auto` (the framework's schema default, which resolves to `stateful`). The server declares `stateless` in source — no tool requests input mid-call — and a value set here overrides it. | `stateless` |
250
279
  | `MCP_AUTH_MODE` | Authentication: `none`, `jwt`, or `oauth`. | `none` |
251
280
  | `MCP_LOG_LEVEL` | Log level (`debug`, `info`, `warning`, `error`, etc.). | `info` |
252
281
  | `LOGS_DIR` | Directory for log files (Node.js only). | `<project-root>/logs` |
@@ -290,8 +319,8 @@ The Dockerfile defaults to HTTP transport, stateless session mode, and logs to `
290
319
 
291
320
  ## Project structure
292
321
 
293
- | Path | Purpose |
294
- |:-----|:--------|
322
+ | Directory | Purpose |
323
+ |:----------|:--------|
295
324
  | `src/index.ts` | `createApp()` entry point — registers tools/resources and initializes the Pixoo service. |
296
325
  | `src/config/` | Server-specific environment variable parsing and validation with Zod. |
297
326
  | `src/mcp-server/tools/` | Tool definitions (`*.tool.ts`). |
@@ -307,11 +336,11 @@ See [`CLAUDE.md`/`AGENTS.md`](./CLAUDE.md) for development guidelines and archit
307
336
  - Handlers throw, framework catches — no `try/catch` in tool logic
308
337
  - Use `ctx.log` for request-scoped logging, `ctx.state` for tenant-scoped storage
309
338
  - The renderer (`src/renderer/`) is pure — no device dependency, testable without hardware
310
- - All device calls go through `PixooService`; every `PixooResult` is checked
339
+ - All device calls go through `PixooService`; every `PixooResult` is checked — never assume a push succeeded
311
340
 
312
341
  ## Contributing
313
342
 
314
- Issues and pull requests are welcome. Run checks and tests before submitting:
343
+ Issues are welcome. Run checks and tests before submitting:
315
344
 
316
345
  ```sh
317
346
  bun run devcheck
@@ -0,0 +1,36 @@
1
+ ---
2
+ summary: "@cyanheads/mcp-ts-core ^0.13.2 adoption: explicit stateless session mode, a structured -32602 argument-rejection envelope, unset-env normalization for PIXOO_* vars, and the framework skill tree moved to framework-skills/. Claude and Codex plugin manifests now forward PIXOO_IP/PIXOO_SIZE."
3
+ breaking: false
4
+ security: false
5
+ ---
6
+
7
+ # 1.1.2 — 2026-09-16
8
+
9
+ ## Added
10
+
11
+ - **Plugin device config** — the Claude plugin declares `userConfig` (`pixoo_ip`/`pixoo_size`) wired to `PIXOO_IP`/`PIXOO_SIZE`; the Codex plugin forwards both via `env_vars`. A Codex plugin install previously had no way to set the device IP at all.
12
+ - **Server card publishes the resolved session mode** under `_meta["io.github.cyanheads.mcp-ts-core/sessionMode"]` ([mcp-ts-core#387](https://github.com/cyanheads/mcp-ts-core/issues/387)).
13
+
14
+ ## Changed
15
+
16
+ - **Argument rejections carry `structuredContent.error`** (`code: -32602`, `isError: true`) alongside the existing readable text ([mcp-ts-core#377](https://github.com/cyanheads/mcp-ts-core/issues/377)).
17
+ - **An empty or unsubstituted `${…}` `PIXOO_*` value now reads as unset** rather than as a literal value, through `parseEnvConfig` ([mcp-ts-core#427](https://github.com/cyanheads/mcp-ts-core/issues/427)).
18
+ - **Server identity (name/version) resolves from the served package**, not the working directory ([mcp-ts-core#373](https://github.com/cyanheads/mcp-ts-core/issues/373), [mcp-ts-core#374](https://github.com/cyanheads/mcp-ts-core/issues/374)).
19
+ - Framework skill tree moved `skills/` → `framework-skills/` ([mcp-ts-core#428](https://github.com/cyanheads/mcp-ts-core/issues/428)); scripts and `.github/` templates synced to match.
20
+ - Bun engines floor raised to `>=1.4.0`.
21
+
22
+ ## Fixed
23
+
24
+ - **`sessionMode: 'stateless'` is now declared in `src/index.ts`** — an HTTP run with `MCP_SESSION_MODE` unset resolves `stateless` instead of `stateful`; an explicit value still overrides it ([mcp-ts-core#376](https://github.com/cyanheads/mcp-ts-core/issues/376)).
25
+ - **`SIGTERM`/`SIGINT` now end the process explicitly** once shutdown settles, instead of waiting on the event loop to drain ([mcp-ts-core#435](https://github.com/cyanheads/mcp-ts-core/issues/435)).
26
+
27
+ ## Dependencies
28
+
29
+ - `@cyanheads/mcp-ts-core` ^0.12.3 → ^0.13.2
30
+ - `zod` ^4.4.3 → ^4.6.4
31
+ - `sharp` ^0.35.3 → ^0.35.4
32
+ - `@biomejs/biome` 2.5.9 → 2.5.13
33
+ - `@types/node` 26.2.0 → 26.5.1
34
+ - `ignore` ^7.0.6 → ^7.0.9
35
+ - `tsc-alias` ^1.9.2 → ^1.9.5
36
+ - `vitest` ^4.1.11 → ^5.0.0
@@ -0,0 +1,30 @@
1
+ ---
2
+ summary: "Errors thrown in the tool handlers now forward their declared recovery hint to callers; invalid_color hints match what resolveColor accepts; pixoo_control_device drops two error reasons it could never emit (#8). mcp-ts-core ^0.13.2 → ^0.13.6 adds a Recovery: hint on argument rejections and key normalization for tool calls."
3
+ breaking: false
4
+ security: false
5
+ ---
6
+
7
+ # 1.1.3 — 2026-09-21
8
+
9
+ ## Changed
10
+
11
+ - **Declared recovery hints reach the caller** — errors thrown in the tool handlers (`invalid_color`, `unknown_icon`, `invalid_output_path`, local-path `asset_not_found`, and `pixoo_overlay_text`'s `device_unreachable`/`device_rejected`) now carry `error.data.recovery.hint` and a `Recovery:` line in the text; previously those errors carried only `data.reason`.
12
+ - **`invalid_color` hints match what `resolveColor` accepts** — hex (`#RRGGBB`/`#RGB`, `#` optional) or a case-insensitive named color. `pixoo_overlay_text`'s hint used to say named colors were unsupported for overlays, but its handler always resolved them (#8).
13
+ - **`pixoo_control_device` no longer declares `device_unreachable`/`device_rejected`** — its setters report failures in the result instead of throwing, so neither reason could occur; only the advertised `reason` examples change.
14
+ - `pixoo_design_brief`'s color-troubleshooting topic now says named colors are case-insensitive, matching `resolveColor`.
15
+ - Server `instructions` reworded to one two-sentence string, following the framework's instructions-shape guidance.
16
+ - Service-raised error reasons are marked `thrownBy: 'service'` in each tool's error contract — lint metadata only, no wire effect.
17
+ - **Argument rejections carry a schema-derived `Recovery:` hint** naming the tool's accepted keys, and an omitted required enum/literal field now reads as missing rather than as a wrong choice ([cyanheads/mcp-ts-core#445](https://github.com/cyanheads/mcp-ts-core/issues/445), [cyanheads/mcp-ts-core#378](https://github.com/cyanheads/mcp-ts-core/issues/378)).
18
+ - **A case-style variant of a declared argument key is rewritten and accepted** (e.g. `clock_face_id` for `clockFaceId`), and a JSON-stringified array argument is repaired and re-parsed once; an undeclared key is still rejected ([cyanheads/mcp-ts-core#452](https://github.com/cyanheads/mcp-ts-core/issues/452), [cyanheads/mcp-ts-core#453](https://github.com/cyanheads/mcp-ts-core/issues/453), [cyanheads/mcp-ts-core#234](https://github.com/cyanheads/mcp-ts-core/issues/234)).
19
+ - **Tool error text now closes with `(reason <reason>)`**, plus ` · retryable` / ` · not retryable` when the error declares `data.retryable` ([cyanheads/mcp-ts-core#458](https://github.com/cyanheads/mcp-ts-core/issues/458)).
20
+ - `.env.example`/README now note that `MCP_SESSION_MODE=auto` (the framework's schema default) resolves to `stateful`; the shipped value stays `stateless`. `.env.example` documents the commented `MCP_HTTP_RESUMABILITY*` options.
21
+ - Test config hides `pixoo-toolkit`'s dangling sourcemap warnings during tests ([cyanheads/pixoo-toolkit#42](https://github.com/cyanheads/pixoo-toolkit/issues/42)).
22
+ - Repo hygiene: CodeQL workflow added; framework skills, `lint-mcp.ts`, the changelog template, and `devcheck.config.json` synced with mcp-ts-core 0.13.6.
23
+
24
+ ## Dependencies
25
+
26
+ - `@cyanheads/mcp-ts-core` ^0.13.2 → ^0.13.6
27
+ - `zod` ^4.6.4 → ^4.6.5
28
+ - `@biomejs/biome` 2.5.13 → 2.5.14
29
+ - `@types/node` 26.5.1 → 26.6.2
30
+ - `vitest` ^5.0.0 → ^5.0.1
@@ -6,8 +6,8 @@
6
6
 
7
7
  # Required. One-line GitHub Release-style headline. 350 character cap — a
8
8
  # ceiling, not a target. Default short and scannable. Don't pad, don't stitch
9
- # unrelated changes with commas/semicolons into an inventory — pick the
10
- # headline, like a tag's theme line. Quotes required: unquoted YAML treats
9
+ # unrelated changes with commas/semicolons into an inventory — pick the one
10
+ # headline the release is about. Quotes required: unquoted YAML treats
11
11
  # `: ` inside the value as a key separator and fails GitHub's strict parser.
12
12
  summary: ""
13
13
 
@@ -117,30 +117,13 @@ security: false
117
117
  in that unrelated item's metadata.
118
118
 
119
119
  TAG ANNOTATIONS — the annotated tag body renders as the GitHub Release body
120
- via `gh release create --notes-from-tag`. The tag is a derivative of this
121
- changelog entry — a condensed, scannable version, not a copy. Format:
122
-
123
- <theme — omit version number, GitHub prepends it>
124
- ← blank line
125
- <1-2 sentence context: what this release does>
126
- ← blank line
127
- Dependency bumps: ← section header
128
- ← blank line
129
- - `@cyanheads/mcp-ts-core` ^0.9.1 → ^0.9.6 ← bullet
130
- ← blank line
131
- Changed: ← only sections with entries
132
- ← blank line
133
- - `format()` output includes `query` in text mode
134
- ← blank line
135
- Added:
136
- ← blank line
137
- - `manifest.json` scaffolded for MCPB bundle support
138
- - Install badges (Claude Desktop, Cursor, VS Code)
139
- ← blank line
140
- <N> tests pass; `bun run devcheck` clean. ← footer
141
-
142
- Never a flat comma-separated string. Always structured markdown with
143
- sections. The tag must scan well as a rendered GitHub Release page.
120
+ via `gh release create --notes-from-tag`. It is a condensed digest of this
121
+ entry, never a copy, and its format is owned by the `release-and-publish`
122
+ skill (step 4, "Create the annotated tag"): a short subject line without the
123
+ version, flat headline bullets — no Keep-a-Changelog section headers, no
124
+ gates line — at most one deps line, issue backlinks, and the changelog link
125
+ last. In release-PR mode the `git-wrapup` skill authors those bullets as the
126
+ PR body's `## Changes` and the tag copies them.
144
127
  -->
145
128
 
146
129
  ## Added
package/dist/index.js CHANGED
@@ -37,6 +37,8 @@ await createApp({
37
37
  pixooDesignGuideResource,
38
38
  ],
39
39
  prompts: [],
40
+ // No handler requests input mid-call, so nothing needs a 2025-era session.
41
+ sessionMode: 'stateless',
40
42
  /**
41
43
  * The tool and resource surface is fixed at build time — nothing registers or
42
44
  * retires a definition at runtime — so the list results are safe for shared
@@ -51,8 +53,6 @@ await createApp({
51
53
  setup(core) {
52
54
  initPixooService(core.config, core.storage);
53
55
  },
54
- instructions: 'Pixoo LED matrix display server. Use pixoo_design_brief(topic) first to orient on craft guidelines. ' +
55
- 'pixoo_display_text is the 80% case for styled text. pixoo_compose_scene for layered scenes, widgets, and animations. ' +
56
- 'All render tools return a preview image so you can inspect the result before it hits the display.',
56
+ instructions: 'Run pixoo_design_brief with a topic first for craft guidance and live device state, then render with pixoo_display_text for styled text or pixoo_compose_scene for layered scenes, widgets, and animations. Every render tool returns a preview image, so pass push: false to inspect a design before it reaches the Pixoo display.',
57
57
  });
58
58
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;GAGG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACnD,YAAY;AACZ,OAAO,EAAE,wBAAwB,EAAE,MAAM,mEAAmE,CAAC;AAC7G,OAAO,EAAE,yBAAyB,EAAE,MAAM,oEAAoE,CAAC;AAC/G,OAAO,EAAE,kBAAkB,EAAE,MAAM,4DAA4D,CAAC;AAChG,OAAO,EAAE,mBAAmB,EAAE,MAAM,6DAA6D,CAAC;AAClG,QAAQ;AACR,OAAO,EAAE,iBAAiB,EAAE,MAAM,4DAA4D,CAAC;AAC/F,OAAO,EAAE,kBAAkB,EAAE,MAAM,6DAA6D,CAAC;AACjG,OAAO,EAAE,gBAAgB,EAAE,MAAM,2DAA2D,CAAC;AAC7F,OAAO,EAAE,oBAAoB,EAAE,MAAM,+DAA+D,CAAC;AACrG,OAAO,EAAE,gBAAgB,EAAE,MAAM,2DAA2D,CAAC;AAC7F,OAAO,EAAE,gBAAgB,EAAE,MAAM,2DAA2D,CAAC;AAC7F,OAAO,EAAE,cAAc,EAAE,MAAM,yDAAyD,CAAC;AACzF,OAAO,EAAE,gBAAgB,EAAE,MAAM,mCAAmC,CAAC;AAErE,MAAM,SAAS,CAAC;IACd,IAAI,EAAE,kBAAkB;IACxB,KAAK,EAAE,kBAAkB;IACzB,KAAK,EAAE;QACL,gBAAgB;QAChB,iBAAiB;QACjB,cAAc;QACd,gBAAgB;QAChB,kBAAkB;QAClB,oBAAoB;QACpB,gBAAgB;KACjB;IACD,SAAS,EAAE;QACT,yBAAyB;QACzB,mBAAmB;QACnB,kBAAkB;QAClB,wBAAwB;KACzB;IACD,OAAO,EAAE,EAAE;IACX;;;;;OAKG;IACH,UAAU,EAAE;QACV,YAAY,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,QAAQ,EAAE;QACxD,gBAAgB,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,QAAQ,EAAE;QAC5D,0BAA0B,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,QAAQ,EAAE;KACvE;IACD,KAAK,CAAC,IAAI;QACR,gBAAgB,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;IAC9C,CAAC;IACD,YAAY,EACV,sGAAsG;QACtG,uHAAuH;QACvH,mGAAmG;CACtG,CAAC,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;GAGG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACnD,YAAY;AACZ,OAAO,EAAE,wBAAwB,EAAE,MAAM,mEAAmE,CAAC;AAC7G,OAAO,EAAE,yBAAyB,EAAE,MAAM,oEAAoE,CAAC;AAC/G,OAAO,EAAE,kBAAkB,EAAE,MAAM,4DAA4D,CAAC;AAChG,OAAO,EAAE,mBAAmB,EAAE,MAAM,6DAA6D,CAAC;AAClG,QAAQ;AACR,OAAO,EAAE,iBAAiB,EAAE,MAAM,4DAA4D,CAAC;AAC/F,OAAO,EAAE,kBAAkB,EAAE,MAAM,6DAA6D,CAAC;AACjG,OAAO,EAAE,gBAAgB,EAAE,MAAM,2DAA2D,CAAC;AAC7F,OAAO,EAAE,oBAAoB,EAAE,MAAM,+DAA+D,CAAC;AACrG,OAAO,EAAE,gBAAgB,EAAE,MAAM,2DAA2D,CAAC;AAC7F,OAAO,EAAE,gBAAgB,EAAE,MAAM,2DAA2D,CAAC;AAC7F,OAAO,EAAE,cAAc,EAAE,MAAM,yDAAyD,CAAC;AACzF,OAAO,EAAE,gBAAgB,EAAE,MAAM,mCAAmC,CAAC;AAErE,MAAM,SAAS,CAAC;IACd,IAAI,EAAE,kBAAkB;IACxB,KAAK,EAAE,kBAAkB;IACzB,KAAK,EAAE;QACL,gBAAgB;QAChB,iBAAiB;QACjB,cAAc;QACd,gBAAgB;QAChB,kBAAkB;QAClB,oBAAoB;QACpB,gBAAgB;KACjB;IACD,SAAS,EAAE;QACT,yBAAyB;QACzB,mBAAmB;QACnB,kBAAkB;QAClB,wBAAwB;KACzB;IACD,OAAO,EAAE,EAAE;IACX,2EAA2E;IAC3E,WAAW,EAAE,WAAW;IACxB;;;;;OAKG;IACH,UAAU,EAAE;QACV,YAAY,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,QAAQ,EAAE;QACxD,gBAAgB,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,QAAQ,EAAE;QAC5D,0BAA0B,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,QAAQ,EAAE;KACvE;IACD,KAAK,CAAC,IAAI;QACR,gBAAgB,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;IAC9C,CAAC;IACD,YAAY,EACV,qUAAqU;CACxU,CAAC,CAAC"}
@@ -471,26 +471,30 @@ export declare const pixooComposeScene: import("@cyanheads/mcp-ts-core").ToolDef
471
471
  readonly when: "Device is not reachable over the network.";
472
472
  readonly retryable: true;
473
473
  readonly recovery: "Check the device is powered on and on the same network. Retry in a few seconds.";
474
+ readonly thrownBy: "service";
474
475
  }, {
475
476
  readonly reason: "device_rejected";
476
477
  readonly code: JsonRpcErrorCode.ServiceUnavailable;
477
478
  readonly when: "Device firmware returned a non-zero error code.";
478
479
  readonly recovery: "Note the device error code and check the Pixoo documentation.";
480
+ readonly thrownBy: "service";
479
481
  }, {
480
482
  readonly reason: "no_device_configured";
481
483
  readonly code: JsonRpcErrorCode.InvalidParams;
482
484
  readonly when: "PIXOO_IP is not set and push was requested.";
483
485
  readonly recovery: "Run pixoo_discover_devices to find the device IP, then set PIXOO_IP.";
486
+ readonly thrownBy: "service";
484
487
  }, {
485
488
  readonly reason: "asset_not_found";
486
489
  readonly code: JsonRpcErrorCode.NotFound;
487
490
  readonly when: "An image or sprite path could not be read.";
488
491
  readonly recovery: "Verify the file path exists and is readable, or check the URL is reachable.";
492
+ readonly thrownBy: "service";
489
493
  }, {
490
494
  readonly reason: "invalid_color";
491
495
  readonly code: JsonRpcErrorCode.InvalidParams;
492
496
  readonly when: "A color value could not be resolved.";
493
- readonly recovery: "Use #RRGGBB hex or a named color. See pixoo://reference/themes for palettes.";
497
+ readonly recovery: "Use a hex color (#RRGGBB or #RGB, with or without the #) or a case-insensitive named color such as white, orange, or claude. See pixoo://reference/themes for palette colors.";
494
498
  }, {
495
499
  readonly reason: "unknown_icon";
496
500
  readonly code: JsonRpcErrorCode.InvalidParams;
@@ -1 +1 @@
1
- {"version":3,"file":"pixoo-compose-scene.tool.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/pixoo-compose-scene.tool.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAQ,CAAC,EAAE,MAAM,wBAAwB,CAAC;AACjD,OAAO,EAAE,gBAAgB,EAAY,MAAM,+BAA+B,CAAC;AAmT3E,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAiT5B,CAAC"}
1
+ {"version":3,"file":"pixoo-compose-scene.tool.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/pixoo-compose-scene.tool.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAQ,CAAC,EAAE,MAAM,wBAAwB,CAAC;AACjD,OAAO,EAAE,gBAAgB,EAAY,MAAM,+BAA+B,CAAC;AAmT3E,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAwT5B,CAAC"}
@@ -323,8 +323,7 @@ export const pixooComposeScene = tool('pixoo_compose_scene', {
323
323
  output: z
324
324
  .string()
325
325
  .optional()
326
- .describe('Absolute, already-normalized path to save the first frame to, in addition to the ' +
327
- 'PIXOO_OUTPUT_DIR auto-save when that is configured. Both paths are reported in outputFiles.'),
326
+ .describe('Absolute, already-normalized path to save the first frame to, in addition to the PIXOO_OUTPUT_DIR auto-save when that is configured. Both paths are reported in outputFiles.'),
328
327
  }),
329
328
  output: z.object({
330
329
  pushed: z
@@ -371,30 +370,34 @@ export const pixooComposeScene = tool('pixoo_compose_scene', {
371
370
  when: 'Device is not reachable over the network.',
372
371
  retryable: true,
373
372
  recovery: 'Check the device is powered on and on the same network. Retry in a few seconds.',
373
+ thrownBy: 'service',
374
374
  },
375
375
  {
376
376
  reason: 'device_rejected',
377
377
  code: JsonRpcErrorCode.ServiceUnavailable,
378
378
  when: 'Device firmware returned a non-zero error code.',
379
379
  recovery: 'Note the device error code and check the Pixoo documentation.',
380
+ thrownBy: 'service',
380
381
  },
381
382
  {
382
383
  reason: 'no_device_configured',
383
384
  code: JsonRpcErrorCode.InvalidParams,
384
385
  when: 'PIXOO_IP is not set and push was requested.',
385
386
  recovery: 'Run pixoo_discover_devices to find the device IP, then set PIXOO_IP.',
387
+ thrownBy: 'service',
386
388
  },
387
389
  {
388
390
  reason: 'asset_not_found',
389
391
  code: JsonRpcErrorCode.NotFound,
390
392
  when: 'An image or sprite path could not be read.',
391
393
  recovery: 'Verify the file path exists and is readable, or check the URL is reachable.',
394
+ thrownBy: 'service',
392
395
  },
393
396
  {
394
397
  reason: 'invalid_color',
395
398
  code: JsonRpcErrorCode.InvalidParams,
396
399
  when: 'A color value could not be resolved.',
397
- recovery: 'Use #RRGGBB hex or a named color. See pixoo://reference/themes for palettes.',
400
+ recovery: 'Use a hex color (#RRGGBB or #RGB, with or without the #) or a case-insensitive named color such as white, orange, or claude. See pixoo://reference/themes for palette colors.',
398
401
  },
399
402
  {
400
403
  reason: 'unknown_icon',
@@ -430,7 +433,7 @@ export const pixooComposeScene = tool('pixoo_compose_scene', {
430
433
  // collapse traversal segments, passing everything.
431
434
  if (input.output &&
432
435
  (!path.isAbsolute(input.output) || path.normalize(input.output) !== input.output)) {
433
- throw ctx.fail('invalid_output_path', `Invalid output path: "${input.output}". Must be an absolute path with no traversal segments.`);
436
+ throw ctx.fail('invalid_output_path', `Invalid output path: "${input.output}". Must be an absolute path with no traversal segments.`, ctx.recoveryFor('invalid_output_path'));
434
437
  }
435
438
  ctx.log.info('Rendering scene', {
436
439
  elements: input.elements.length,
@@ -449,10 +452,10 @@ export const pixooComposeScene = tool('pixoo_compose_scene', {
449
452
  }
450
453
  catch (err) {
451
454
  if (err instanceof Error && err.message.includes('Unknown color')) {
452
- throw ctx.fail('invalid_color', `${err.message}. Valid named colors: ${validColorNames}. See pixoo://reference/themes for palette colors.`);
455
+ throw ctx.fail('invalid_color', `${err.message}. Valid named colors: ${validColorNames}.`, ctx.recoveryFor('invalid_color'));
453
456
  }
454
457
  if (err instanceof McpError && err.data?.['reason'] === 'unknown_icon') {
455
- throw ctx.fail('unknown_icon', `${err.message} Valid icons: ${validIconNames}. See pixoo://reference/icons.`);
458
+ throw ctx.fail('unknown_icon', `${err.message} Valid icons: ${validIconNames}.`, ctx.recoveryFor('unknown_icon'));
456
459
  }
457
460
  throw err;
458
461
  }