@cyanheads/pixoo-mcp-server 1.1.2 → 1.1.4

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 (45) hide show
  1. package/AGENTS.md +36 -19
  2. package/CLAUDE.md +36 -19
  3. package/README.md +9 -8
  4. package/changelog/1.1.x/1.1.3.md +30 -0
  5. package/changelog/1.1.x/1.1.4.md +22 -0
  6. package/changelog/template.md +7 -7
  7. package/dist/index.js +1 -3
  8. package/dist/index.js.map +1 -1
  9. package/dist/mcp-server/tools/definitions/pixoo-compose-scene.tool.d.ts +11 -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 +16 -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 +4 -12
  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 +21 -33
  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 +10 -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 +12 -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 +7 -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 +24 -10
  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 +9 -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 +11 -1
  33. package/dist/mcp-server/tools/definitions/pixoo-push-image.tool.js.map +1 -1
  34. package/dist/renderer/remote-image.d.ts.map +1 -1
  35. package/dist/renderer/remote-image.js +22 -6
  36. package/dist/renderer/remote-image.js.map +1 -1
  37. package/dist/renderer/scene-renderer.d.ts.map +1 -1
  38. package/dist/renderer/scene-renderer.js +26 -3
  39. package/dist/renderer/scene-renderer.js.map +1 -1
  40. package/dist/services/pixoo/pixoo-service.d.ts +14 -1
  41. package/dist/services/pixoo/pixoo-service.d.ts.map +1 -1
  42. package/dist/services/pixoo/pixoo-service.js +38 -15
  43. package/dist/services/pixoo/pixoo-service.js.map +1 -1
  44. package/package.json +6 -6
  45. package/server.json +3 -3
package/AGENTS.md CHANGED
@@ -1,11 +1,11 @@
1
1
  # Developer Protocol
2
2
 
3
3
  **Server:** pixoo-mcp-server
4
- **Version:** 1.1.2
5
- **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.2`
4
+ **Version:** 1.1.4
5
+ **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.6`
6
6
  **Engines:** Bun ≥1.4.0, Node ≥24.0.0
7
7
  **MCP SDK:** `@modelcontextprotocol/server` ^2.0.0 (protocol revision 2026-07-28 alongside the 2025 era)
8
- **Zod:** ^4.6.4
8
+ **Zod:** ^4.6.5
9
9
 
10
10
  > **Read the framework docs first:** `node_modules/@cyanheads/mcp-ts-core/CLAUDE.md` contains the full API reference — builders, Context, error codes, exports, patterns. This file covers server-specific conventions only.
11
11
 
@@ -165,6 +165,7 @@ Handlers receive a unified `ctx` object. Key properties used by this server:
165
165
  | `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
166
  | `ctx.enrich` | Success-path agent context — `.notice()` / `.total()` / `.echo()` / `.truncated()`. Lands only when the definition declares an `enrichment` block. |
167
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. |
168
169
  | `ctx.state` | Tenant-scoped KV — `.get`, `.set(key, value, { ttl? })`, `.delete`, `.getMany`, `.list`. Keys are validated; colons are not legal separators. |
169
170
  | `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. |
170
171
  | `ctx.signal` | `AbortSignal` for cancellation. Pass it to any long-running I/O. |
@@ -179,28 +180,36 @@ Handlers throw — the framework catches, classifies, and formats.
179
180
 
180
181
  Pixoo-specific error reasons declared on tools:
181
182
 
182
- | Reason | Code | When |
183
- |:-------|:-----|:-----|
184
- | `device_unreachable` | `ServiceUnavailable` | Toolkit result kind `network`/`timeout` |
185
- | `device_http_error` | `ServiceUnavailable` | Non-2xx from the device's HTTP server |
186
- | `device_rejected` | `ServiceUnavailable` | Firmware returned non-zero `error_code` |
187
- | `no_device_configured` | `InvalidParams` | Device tool called without `PIXOO_IP` |
188
- | `asset_not_found` | `NotFound` | Image/sprite path or URL unreadable |
189
- | `invalid_color` | `InvalidParams` | `resolveColor` throw — invalid color name or format |
190
- | `unknown_icon` | `InvalidParams` | Icon name not in registry |
191
- | `discovery_failed` | `ServiceUnavailable` | Divoom cloud unreachable |
183
+ | Reason | Code | When | `retryable` |
184
+ |:-------|:-----|:-----|:------------|
185
+ | `device_unreachable` | `ServiceUnavailable` | Toolkit result kind `network`/`timeout` | `true` |
186
+ | `device_http_error` | `ServiceUnavailable` | Non-2xx from the device's HTTP server | `true` for 408, 429, 500, 502–504; `false` otherwise |
187
+ | `device_rejected` | `ServiceUnavailable` | Firmware returned non-zero `error_code` | — |
188
+ | `no_device_configured` | `InvalidParams` | Device tool called without `PIXOO_IP` | — |
189
+ | `asset_not_found` | `NotFound` | Image/sprite path or URL unreadable | — |
190
+ | `invalid_color` | `InvalidParams` | `resolveColor` throw — invalid color name or format | — |
191
+ | `unknown_icon` | `InvalidParams` | Icon name not in registry | — |
192
+ | `discovery_failed` | `ServiceUnavailable` | Divoom cloud unreachable | `true` |
193
+
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, and every such site forwards its declared recovery with `ctx.recoveryFor`. A computed reason forwards the same way (`ctx.recoveryFor(reason)`); `lint:mcp` skips a definition whose `ctx.fail` reason is non-literal, so a clean lint says nothing about those sites.
195
+
196
+ 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.
192
197
 
193
198
  ```ts
194
199
  import { JsonRpcErrorCode } from '@cyanheads/mcp-ts-core/errors';
195
200
 
196
201
  errors: [
197
202
  { reason: 'no_device_configured', code: JsonRpcErrorCode.InvalidParams,
198
- when: 'PIXOO_IP not configured',
199
- recovery: 'Run pixoo_discover_devices to find device IPs, then set PIXOO_IP.' },
200
- { reason: 'device_unreachable', code: JsonRpcErrorCode.ServiceUnavailable,
201
- when: 'Device network timeout or connection refused',
202
- recovery: 'Check the device is on the same LAN and powered on, then retry.' },
203
+ when: 'PIXOO_IP is not set.',
204
+ recovery: 'Run pixoo_discover_devices to find the device IP, then set PIXOO_IP.',
205
+ thrownBy: 'service' },
206
+ { reason: 'invalid_color', code: JsonRpcErrorCode.InvalidParams,
207
+ when: 'The color value could not be resolved.',
208
+ recovery: 'Use a hex color (#RRGGBB or #RGB, with or without the #) or a case-insensitive named color such as white, orange, or claude.' },
203
209
  ],
210
+
211
+ // in the handler
212
+ throw ctx.fail('invalid_color', `Invalid color "${input.color}"`, ctx.recoveryFor('invalid_color'));
204
213
  ```
205
214
 
206
215
  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.
@@ -284,7 +293,7 @@ Available skills:
284
293
  | `code-simplifier` | Post-session cleanup against `git diff` — modernize syntax, consolidate duplication, align with the codebase |
285
294
  | `polish-docs-meta` | Finalize docs, README, metadata, and agent protocol for shipping |
286
295
  | `git-wrapup` | Land working-tree changes as a commit stack — version bump, changelog, verify, commit by concern, release commit on top. No tag, no push to main; opens the release PR when the project declares release PR mode |
287
- | `release-pr-review` | Review pass on an open release PR — simplifier + correctness review, fixup commits autosquashed into the stack, PR body kept in sync. Release PR mode only |
296
+ | `release-pr-review` | Review pass on an open release PR — simplifier + correctness review, fixes as ordinary commits on top of the stack, PR body kept in sync. Release PR mode only |
288
297
  | `release-and-publish` | Fast-forward merge (release PR mode) + tag + push + npm + MCP Registry + GH Release + Docker. Picks up from `git-wrapup` |
289
298
  | `maintenance` | Investigate changelogs, adopt upstream changes, sync skills to agent dirs |
290
299
  | `orchestrations` | Chain task skills into a gated multi-phase pipeline — build-out, QA-fix, update-ship — when you can spawn sub-agents |
@@ -331,6 +340,8 @@ Available skills:
331
340
  | `bun run changelog:check` | Verify `CHANGELOG.md` is in sync (used by devcheck) |
332
341
  | `bun run bundle` | Build, pack, and clean a `.mcpb` for one-click Claude Desktop install |
333
342
 
343
+ **CI is one file.** `.github/workflows/codeql.yml` is the only GitHub Actions workflow: CodeQL is GitHub-owned end to end, and the file runs only while the repo's CodeQL *default setup* is turned off. Verification — `devcheck`, tests, the release gates — runs locally; don't add a workflow that re-runs it.
344
+
334
345
  ---
335
346
 
336
347
  ## Bundling
@@ -351,6 +362,12 @@ Each per-version file opens with YAML frontmatter: `summary` (required, ≤350 c
351
362
 
352
363
  ---
353
364
 
365
+ ## Publishing
366
+
367
+ **Every release goes through a release PR, straight-through** — `git-wrapup`'s "Release PR mode", mode `straight-through`. One run: `git-wrapup` lands the commit stack on `release/<version>`, pushes it, and opens the PR (title = the release commit subject, body = the changelog entry plus a gates section); `release-and-publish` then fast-forwards `main` locally with `git merge --ff-only`, creates the tag on `main`'s tip, pushes `main` and the tag, deletes the branch, and publishes. A caller's brief may run a given release as `gated` instead — a `release-pr-review` pass on the open PR before `release-and-publish`. **Never merge through the GitHub UI or `gh pr merge`**: squash and rebase-merge are disabled in the repo settings because both rewrite the stack (rebase-merge also strips the SSH signatures), and a merge commit breaks the linear history.
368
+
369
+ ---
370
+
354
371
  ## Checklist
355
372
 
356
373
  - [ ] 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()`)
package/CLAUDE.md CHANGED
@@ -1,11 +1,11 @@
1
1
  # Developer Protocol
2
2
 
3
3
  **Server:** pixoo-mcp-server
4
- **Version:** 1.1.2
5
- **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.2`
4
+ **Version:** 1.1.4
5
+ **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.6`
6
6
  **Engines:** Bun ≥1.4.0, Node ≥24.0.0
7
7
  **MCP SDK:** `@modelcontextprotocol/server` ^2.0.0 (protocol revision 2026-07-28 alongside the 2025 era)
8
- **Zod:** ^4.6.4
8
+ **Zod:** ^4.6.5
9
9
 
10
10
  > **Read the framework docs first:** `node_modules/@cyanheads/mcp-ts-core/CLAUDE.md` contains the full API reference — builders, Context, error codes, exports, patterns. This file covers server-specific conventions only.
11
11
 
@@ -165,6 +165,7 @@ Handlers receive a unified `ctx` object. Key properties used by this server:
165
165
  | `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
166
  | `ctx.enrich` | Success-path agent context — `.notice()` / `.total()` / `.echo()` / `.truncated()`. Lands only when the definition declares an `enrichment` block. |
167
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. |
168
169
  | `ctx.state` | Tenant-scoped KV — `.get`, `.set(key, value, { ttl? })`, `.delete`, `.getMany`, `.list`. Keys are validated; colons are not legal separators. |
169
170
  | `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. |
170
171
  | `ctx.signal` | `AbortSignal` for cancellation. Pass it to any long-running I/O. |
@@ -179,28 +180,36 @@ Handlers throw — the framework catches, classifies, and formats.
179
180
 
180
181
  Pixoo-specific error reasons declared on tools:
181
182
 
182
- | Reason | Code | When |
183
- |:-------|:-----|:-----|
184
- | `device_unreachable` | `ServiceUnavailable` | Toolkit result kind `network`/`timeout` |
185
- | `device_http_error` | `ServiceUnavailable` | Non-2xx from the device's HTTP server |
186
- | `device_rejected` | `ServiceUnavailable` | Firmware returned non-zero `error_code` |
187
- | `no_device_configured` | `InvalidParams` | Device tool called without `PIXOO_IP` |
188
- | `asset_not_found` | `NotFound` | Image/sprite path or URL unreadable |
189
- | `invalid_color` | `InvalidParams` | `resolveColor` throw — invalid color name or format |
190
- | `unknown_icon` | `InvalidParams` | Icon name not in registry |
191
- | `discovery_failed` | `ServiceUnavailable` | Divoom cloud unreachable |
183
+ | Reason | Code | When | `retryable` |
184
+ |:-------|:-----|:-----|:------------|
185
+ | `device_unreachable` | `ServiceUnavailable` | Toolkit result kind `network`/`timeout` | `true` |
186
+ | `device_http_error` | `ServiceUnavailable` | Non-2xx from the device's HTTP server | `true` for 408, 429, 500, 502–504; `false` otherwise |
187
+ | `device_rejected` | `ServiceUnavailable` | Firmware returned non-zero `error_code` | — |
188
+ | `no_device_configured` | `InvalidParams` | Device tool called without `PIXOO_IP` | — |
189
+ | `asset_not_found` | `NotFound` | Image/sprite path or URL unreadable | — |
190
+ | `invalid_color` | `InvalidParams` | `resolveColor` throw — invalid color name or format | — |
191
+ | `unknown_icon` | `InvalidParams` | Icon name not in registry | — |
192
+ | `discovery_failed` | `ServiceUnavailable` | Divoom cloud unreachable | `true` |
193
+
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, and every such site forwards its declared recovery with `ctx.recoveryFor`. A computed reason forwards the same way (`ctx.recoveryFor(reason)`); `lint:mcp` skips a definition whose `ctx.fail` reason is non-literal, so a clean lint says nothing about those sites.
195
+
196
+ 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.
192
197
 
193
198
  ```ts
194
199
  import { JsonRpcErrorCode } from '@cyanheads/mcp-ts-core/errors';
195
200
 
196
201
  errors: [
197
202
  { reason: 'no_device_configured', code: JsonRpcErrorCode.InvalidParams,
198
- when: 'PIXOO_IP not configured',
199
- recovery: 'Run pixoo_discover_devices to find device IPs, then set PIXOO_IP.' },
200
- { reason: 'device_unreachable', code: JsonRpcErrorCode.ServiceUnavailable,
201
- when: 'Device network timeout or connection refused',
202
- recovery: 'Check the device is on the same LAN and powered on, then retry.' },
203
+ when: 'PIXOO_IP is not set.',
204
+ recovery: 'Run pixoo_discover_devices to find the device IP, then set PIXOO_IP.',
205
+ thrownBy: 'service' },
206
+ { reason: 'invalid_color', code: JsonRpcErrorCode.InvalidParams,
207
+ when: 'The color value could not be resolved.',
208
+ recovery: 'Use a hex color (#RRGGBB or #RGB, with or without the #) or a case-insensitive named color such as white, orange, or claude.' },
203
209
  ],
210
+
211
+ // in the handler
212
+ throw ctx.fail('invalid_color', `Invalid color "${input.color}"`, ctx.recoveryFor('invalid_color'));
204
213
  ```
205
214
 
206
215
  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.
@@ -284,7 +293,7 @@ Available skills:
284
293
  | `code-simplifier` | Post-session cleanup against `git diff` — modernize syntax, consolidate duplication, align with the codebase |
285
294
  | `polish-docs-meta` | Finalize docs, README, metadata, and agent protocol for shipping |
286
295
  | `git-wrapup` | Land working-tree changes as a commit stack — version bump, changelog, verify, commit by concern, release commit on top. No tag, no push to main; opens the release PR when the project declares release PR mode |
287
- | `release-pr-review` | Review pass on an open release PR — simplifier + correctness review, fixup commits autosquashed into the stack, PR body kept in sync. Release PR mode only |
296
+ | `release-pr-review` | Review pass on an open release PR — simplifier + correctness review, fixes as ordinary commits on top of the stack, PR body kept in sync. Release PR mode only |
288
297
  | `release-and-publish` | Fast-forward merge (release PR mode) + tag + push + npm + MCP Registry + GH Release + Docker. Picks up from `git-wrapup` |
289
298
  | `maintenance` | Investigate changelogs, adopt upstream changes, sync skills to agent dirs |
290
299
  | `orchestrations` | Chain task skills into a gated multi-phase pipeline — build-out, QA-fix, update-ship — when you can spawn sub-agents |
@@ -331,6 +340,8 @@ Available skills:
331
340
  | `bun run changelog:check` | Verify `CHANGELOG.md` is in sync (used by devcheck) |
332
341
  | `bun run bundle` | Build, pack, and clean a `.mcpb` for one-click Claude Desktop install |
333
342
 
343
+ **CI is one file.** `.github/workflows/codeql.yml` is the only GitHub Actions workflow: CodeQL is GitHub-owned end to end, and the file runs only while the repo's CodeQL *default setup* is turned off. Verification — `devcheck`, tests, the release gates — runs locally; don't add a workflow that re-runs it.
344
+
334
345
  ---
335
346
 
336
347
  ## Bundling
@@ -351,6 +362,12 @@ Each per-version file opens with YAML frontmatter: `summary` (required, ≤350 c
351
362
 
352
363
  ---
353
364
 
365
+ ## Publishing
366
+
367
+ **Every release goes through a release PR, straight-through** — `git-wrapup`'s "Release PR mode", mode `straight-through`. One run: `git-wrapup` lands the commit stack on `release/<version>`, pushes it, and opens the PR (title = the release commit subject, body = the changelog entry plus a gates section); `release-and-publish` then fast-forwards `main` locally with `git merge --ff-only`, creates the tag on `main`'s tip, pushes `main` and the tag, deletes the branch, and publishes. A caller's brief may run a given release as `gated` instead — a `release-pr-review` pass on the open PR before `release-and-publish`. **Never merge through the GitHub UI or `gh pr merge`**: squash and rebase-merge are disabled in the repo settings because both rewrite the stack (rebase-merge also strips the SSH signatures), and a merge commit breaks the linear history.
368
+
369
+ ---
370
+
354
371
  ## Checklist
355
372
 
356
373
  - [ ] 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()`)
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.2-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.4-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
 
@@ -66,7 +66,8 @@ All resource data is also reachable via tools. `pixoo_design_brief` surfaces the
66
66
  - Up to 50 layered elements rendered back-to-front: `text`, `icon`, `rect`, `circle`, `line`, `progress`, `sparkline`, `bitmap`, `pixels`, `image`, `sprite`
67
67
  - Background: solid color, gradient (vertical, horizontal, or radial), or named theme
68
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
69
+ - `image` elements accept an absolute local path or an https URL; `sprite` elements take an absolute local path; a supplied `output` path must be absolute with no traversal segments
70
+ - `opacity` (0–100) blends any element, images included, over the layers beneath it
70
71
  - 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
72
  - Typed failures for `asset_not_found`, `invalid_color`, and `unknown_icon`, alongside the shared device-error reasons
72
73
 
@@ -74,7 +75,7 @@ All resource data is also reachable via tools. `pixoo_design_brief` surfaces the
74
75
 
75
76
  ### `pixoo_push_image` <sub>tool</sub>
76
77
 
77
- - Accepts an absolute local file path or an https (not http) URL
78
+ - Accepts an absolute local file path or an https (not http) URL; a URL response is capped at 10 MB, enforced while the body streams
78
79
  - Three fit modes: `contain` (letterbox), `cover` (crop to fill), `fill` (stretch)
79
80
  - Three resize kernels: `nearest` for pixel art (default), `lanczos3` for photos, `mitchell` for a balance
80
81
  - Returns the exact resized result as an image content block before it is pushed
@@ -85,7 +86,7 @@ All resource data is also reachable via tools. `pixoo_design_brief` surfaces the
85
86
 
86
87
  - `mode: "set"` adds or updates an overlay on one of 20 independent slots (`id` 0–19); `mode: "clear"` removes it
87
88
  - 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
+ - Configurable `x`/`y` (0–64), scroll `direction` (`left`/`right`), `speed` (0–100), and `align`; color accepts hex (`#RRGGBB` or `#RGB`) or a named color, the same as the render tools
89
90
  - Device-rendered, not previewable — for styled, previewable text use `pixoo_display_text`
90
91
 
91
92
  ---
@@ -93,7 +94,7 @@ All resource data is also reachable via tools. `pixoo_design_brief` surfaces the
93
94
  ### `pixoo_control_device` <sub>tool</sub>
94
95
 
95
96
  - 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
+ - `applied` lists which requested settings succeeded; a failed setting is omitted from `applied` and reported in a `notice` instead of failing the call — every failed setting in one call, each with its failure kind and message
97
98
  - Always returns current `reachable`, `channel`, `brightness`, `screenOn`, and `clockId` (the latter three absent when the device is unreachable)
98
99
 
99
100
  ---
@@ -162,8 +163,8 @@ Agent-friendly output:
162
163
 
163
164
  - 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
165
  - 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
166
+ - Device truth — `pushed` reflects the device ACK; `deviceState` after a push reports the channel, brightness, and screen state the device returned
167
+ - Render without a device — `push: false` renders and returns the preview with no device reachable; a push to an unreachable device fails with a retryable `device_unreachable` error
167
168
 
168
169
  ## Getting started
169
170
 
@@ -275,7 +276,7 @@ All configuration is validated at startup via Zod schemas in `src/config/server-
275
276
  | `PIXOO_PUSH_MIN_INTERVAL_MS` | Minimum interval between device pushes in milliseconds. Prevents device freeze from rapid-fire commands. | `1000` |
276
277
  | `MCP_TRANSPORT_TYPE` | Transport: `stdio` or `http`. | `stdio` |
277
278
  | `MCP_HTTP_PORT` | HTTP server port. | `3010` |
278
- | `MCP_SESSION_MODE` | HTTP session handling: `stateful`, `stateless`, or `auto`. The server declares `stateless` in source — no tool requests input mid-call — and a value set here overrides it. | `stateless` |
279
+ | `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` |
279
280
  | `MCP_AUTH_MODE` | Authentication: `none`, `jwt`, or `oauth`. | `none` |
280
281
  | `MCP_LOG_LEVEL` | Log level (`debug`, `info`, `warning`, `error`, etc.). | `info` |
281
282
  | `LOGS_DIR` | Directory for log files (Node.js only). | `<project-root>/logs` |
@@ -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
@@ -0,0 +1,22 @@
1
+ ---
2
+ summary: "Push-capable tools now declare device_http_error and carry retryable; pixoo_compose_scene's image opacity and missing local-asset errors are fixed; pixoo_control_device no longer drops failed-setter notices; pixoo_push_image's fetch cap now holds while the body streams."
3
+ breaking: false
4
+ security: true
5
+ ---
6
+
7
+ # 1.1.4 — 2026-09-23
8
+
9
+ ## Changed
10
+
11
+ - Entry-point session-mode test retries on a fresh port when the child exits before serving, instead of waiting out the full boot deadline.
12
+
13
+ ## Fixed
14
+
15
+ - **`pixoo_display_text`, `pixoo_compose_scene`, and `pixoo_push_image` now declare `device_http_error` and carry `data.retryable` on push failures** — `mapFailure` threw through the raw `serviceUnavailable()` factory, bypassing the tool contract ([#11](https://github.com/cyanheads/pixoo-mcp-server/issues/11)).
16
+ - **`pixoo_compose_scene`'s `image` elements honor `opacity`** — the blit went to the base canvas instead of the opacity-compositing scratch canvas, so opacity was silently dropped ([#4](https://github.com/cyanheads/pixoo-mcp-server/issues/4)).
17
+ - **A missing local `image` or `sprite` path in `pixoo_compose_scene` now fails as `asset_not_found`** instead of an unclassified `InternalError` ([#12](https://github.com/cyanheads/pixoo-mcp-server/issues/12)).
18
+ - **`pixoo_control_device` no longer drops failed-setter notices** — the tool declared no `enrichment` block, so `ctx.enrich.notice` calls were discarded; failures across multiple setters in one call now compose into a single notice ([#13](https://github.com/cyanheads/pixoo-mcp-server/issues/13)).
19
+
20
+ ## Security
21
+
22
+ - **`pixoo_push_image`'s 10 MB fetch cap now holds while the body streams** — a URL response with no `content-length` header was fully buffered before the size check fired, allowing unbounded memory use ([#7](https://github.com/cyanheads/pixoo-mcp-server/issues/7)).
@@ -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
 
@@ -119,11 +119,11 @@ security: false
119
119
  TAG ANNOTATIONS — the annotated tag body renders as the GitHub Release body
120
120
  via `gh release create --notes-from-tag`. It is a condensed digest of this
121
121
  entry, never a copy, and its format is owned by the `release-and-publish`
122
- skill (step 4, "Create the annotated tag"): the entry's `summary:` as the
123
- theme line without the version, flat headline bullets — no Keep-a-Changelog
124
- section headers, no gates line — at most one deps line, issue backlinks,
125
- and the changelog link last. In release-PR mode the `git-wrapup` skill
126
- authors that digest as the PR body's `## Changes` and the tag copies it.
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.
127
127
  -->
128
128
 
129
129
  ## Added
package/dist/index.js CHANGED
@@ -53,8 +53,6 @@ await createApp({
53
53
  setup(core) {
54
54
  initPixooService(core.config, core.storage);
55
55
  },
56
- instructions: 'Pixoo LED matrix display server. Use pixoo_design_brief(topic) first to orient on craft guidelines. ' +
57
- 'pixoo_display_text is the 80% case for styled text. pixoo_compose_scene for layered scenes, widgets, and animations. ' +
58
- '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.',
59
57
  });
60
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,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,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,36 @@ 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";
475
+ }, {
476
+ readonly reason: "device_http_error";
477
+ readonly code: JsonRpcErrorCode.ServiceUnavailable;
478
+ readonly when: "The device answered with a non-2xx HTTP status (retryable for 408, 429, 500, and 502–504).";
479
+ readonly recovery: "The device may be busy or rebooting; wait a few seconds and retry. If it persists, run pixoo_discover_devices to confirm PIXOO_IP points at the Pixoo.";
480
+ readonly thrownBy: "service";
474
481
  }, {
475
482
  readonly reason: "device_rejected";
476
483
  readonly code: JsonRpcErrorCode.ServiceUnavailable;
477
484
  readonly when: "Device firmware returned a non-zero error code.";
478
485
  readonly recovery: "Note the device error code and check the Pixoo documentation.";
486
+ readonly thrownBy: "service";
479
487
  }, {
480
488
  readonly reason: "no_device_configured";
481
489
  readonly code: JsonRpcErrorCode.InvalidParams;
482
490
  readonly when: "PIXOO_IP is not set and push was requested.";
483
491
  readonly recovery: "Run pixoo_discover_devices to find the device IP, then set PIXOO_IP.";
492
+ readonly thrownBy: "service";
484
493
  }, {
485
494
  readonly reason: "asset_not_found";
486
495
  readonly code: JsonRpcErrorCode.NotFound;
487
496
  readonly when: "An image or sprite path could not be read.";
488
497
  readonly recovery: "Verify the file path exists and is readable, or check the URL is reachable.";
498
+ readonly thrownBy: "service";
489
499
  }, {
490
500
  readonly reason: "invalid_color";
491
501
  readonly code: JsonRpcErrorCode.InvalidParams;
492
502
  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.";
503
+ 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
504
  }, {
495
505
  readonly reason: "unknown_icon";
496
506
  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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAgU5B,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,41 @@ 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
+ },
375
+ {
376
+ reason: 'device_http_error',
377
+ code: JsonRpcErrorCode.ServiceUnavailable,
378
+ when: 'The device answered with a non-2xx HTTP status (retryable for 408, 429, 500, and 502–504).',
379
+ recovery: 'The device may be busy or rebooting; wait a few seconds and retry. If it persists, run pixoo_discover_devices to confirm PIXOO_IP points at the Pixoo.',
380
+ thrownBy: 'service',
374
381
  },
375
382
  {
376
383
  reason: 'device_rejected',
377
384
  code: JsonRpcErrorCode.ServiceUnavailable,
378
385
  when: 'Device firmware returned a non-zero error code.',
379
386
  recovery: 'Note the device error code and check the Pixoo documentation.',
387
+ thrownBy: 'service',
380
388
  },
381
389
  {
382
390
  reason: 'no_device_configured',
383
391
  code: JsonRpcErrorCode.InvalidParams,
384
392
  when: 'PIXOO_IP is not set and push was requested.',
385
393
  recovery: 'Run pixoo_discover_devices to find the device IP, then set PIXOO_IP.',
394
+ thrownBy: 'service',
386
395
  },
387
396
  {
388
397
  reason: 'asset_not_found',
389
398
  code: JsonRpcErrorCode.NotFound,
390
399
  when: 'An image or sprite path could not be read.',
391
400
  recovery: 'Verify the file path exists and is readable, or check the URL is reachable.',
401
+ thrownBy: 'service',
392
402
  },
393
403
  {
394
404
  reason: 'invalid_color',
395
405
  code: JsonRpcErrorCode.InvalidParams,
396
406
  when: 'A color value could not be resolved.',
397
- recovery: 'Use #RRGGBB hex or a named color. See pixoo://reference/themes for palettes.',
407
+ 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
408
  },
399
409
  {
400
410
  reason: 'unknown_icon',
@@ -430,7 +440,7 @@ export const pixooComposeScene = tool('pixoo_compose_scene', {
430
440
  // collapse traversal segments, passing everything.
431
441
  if (input.output &&
432
442
  (!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.`);
443
+ 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
444
  }
435
445
  ctx.log.info('Rendering scene', {
436
446
  elements: input.elements.length,
@@ -449,10 +459,10 @@ export const pixooComposeScene = tool('pixoo_compose_scene', {
449
459
  }
450
460
  catch (err) {
451
461
  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.`);
462
+ throw ctx.fail('invalid_color', `${err.message}. Valid named colors: ${validColorNames}.`, ctx.recoveryFor('invalid_color'));
453
463
  }
454
464
  if (err instanceof McpError && err.data?.['reason'] === 'unknown_icon') {
455
- throw ctx.fail('unknown_icon', `${err.message} Valid icons: ${validIconNames}. See pixoo://reference/icons.`);
465
+ throw ctx.fail('unknown_icon', `${err.message} Valid icons: ${validIconNames}.`, ctx.recoveryFor('unknown_icon'));
456
466
  }
457
467
  throw err;
458
468
  }