@cyanheads/pixoo-mcp-server 1.2.2 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/AGENTS.md +13 -9
  2. package/CLAUDE.md +13 -9
  3. package/Dockerfile +63 -46
  4. package/README.md +7 -7
  5. package/changelog/1.3.x/1.3.0.md +35 -0
  6. package/dist/config/server-config.d.ts +1 -1
  7. package/dist/config/server-config.d.ts.map +1 -1
  8. package/dist/config/server-config.js +6 -1
  9. package/dist/config/server-config.js.map +1 -1
  10. package/dist/mcp-server/resources/definitions/pixoo-design-guide.resource.js +1 -1
  11. package/dist/mcp-server/tools/asset-source-schema.d.ts +13 -0
  12. package/dist/mcp-server/tools/asset-source-schema.d.ts.map +1 -0
  13. package/dist/mcp-server/tools/asset-source-schema.js +25 -0
  14. package/dist/mcp-server/tools/asset-source-schema.js.map +1 -0
  15. package/dist/mcp-server/tools/definitions/pixoo-compose-scene.tool.d.ts +43 -53
  16. package/dist/mcp-server/tools/definitions/pixoo-compose-scene.tool.d.ts.map +1 -1
  17. package/dist/mcp-server/tools/definitions/pixoo-compose-scene.tool.js +102 -74
  18. package/dist/mcp-server/tools/definitions/pixoo-compose-scene.tool.js.map +1 -1
  19. package/dist/mcp-server/tools/definitions/pixoo-design-brief.tool.js +1 -1
  20. package/dist/mcp-server/tools/definitions/pixoo-discover-devices.tool.d.ts +1 -1
  21. package/dist/mcp-server/tools/definitions/pixoo-discover-devices.tool.js +1 -1
  22. package/dist/mcp-server/tools/definitions/pixoo-discover-devices.tool.js.map +1 -1
  23. package/dist/mcp-server/tools/definitions/pixoo-display-text.tool.d.ts +6 -9
  24. package/dist/mcp-server/tools/definitions/pixoo-display-text.tool.d.ts.map +1 -1
  25. package/dist/mcp-server/tools/definitions/pixoo-display-text.tool.js +26 -20
  26. package/dist/mcp-server/tools/definitions/pixoo-display-text.tool.js.map +1 -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 +1 -3
  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.map +1 -1
  31. package/dist/mcp-server/tools/definitions/pixoo-push-image.tool.js +5 -6
  32. package/dist/mcp-server/tools/definitions/pixoo-push-image.tool.js.map +1 -1
  33. package/dist/renderer/remote-image.d.ts +1 -1
  34. package/dist/renderer/remote-image.d.ts.map +1 -1
  35. package/dist/renderer/remote-image.js +6 -6
  36. package/dist/renderer/remote-image.js.map +1 -1
  37. package/dist/renderer/scene-renderer.d.ts +14 -8
  38. package/dist/renderer/scene-renderer.d.ts.map +1 -1
  39. package/dist/renderer/scene-renderer.js +17 -17
  40. package/dist/renderer/scene-renderer.js.map +1 -1
  41. package/dist/renderer/text-engine.d.ts +22 -12
  42. package/dist/renderer/text-engine.d.ts.map +1 -1
  43. package/dist/renderer/text-engine.js +38 -26
  44. package/dist/renderer/text-engine.js.map +1 -1
  45. package/dist/services/pixoo/pixoo-service.d.ts.map +1 -1
  46. package/dist/services/pixoo/pixoo-service.js +9 -33
  47. package/dist/services/pixoo/pixoo-service.js.map +1 -1
  48. package/package.json +2 -2
  49. package/server.json +3 -3
package/AGENTS.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # Developer Protocol
2
2
 
3
3
  **Server:** pixoo-mcp-server
4
- **Version:** 1.2.2
5
- **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.9`
4
+ **Version:** 1.3.0
5
+ **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.10`
6
6
  **Engines:** Bun ≥1.4.0, Node ≥24.0.0
7
7
  **MCP SDK:** `@modelcontextprotocol/server` ^2.1.0 (protocol revision 2026-07-28 alongside the 2025 era)
8
8
  **Zod:** ^4.6.5
@@ -118,6 +118,7 @@ export const pixooDeviceStatusResource = resource('pixoo://device/status', {
118
118
 
119
119
  ```ts
120
120
  // src/config/server-config.ts
121
+ import * as path from 'node:path';
121
122
  import { z } from '@cyanheads/mcp-ts-core';
122
123
  import { parseEnvConfig } from '@cyanheads/mcp-ts-core/config';
123
124
 
@@ -126,7 +127,8 @@ const ServerConfigSchema = z.object({
126
127
  pixooSize: z.coerce.number()
127
128
  .refine((v) => v === 16 || v === 32 || v === 64)
128
129
  .default(64).describe('Display size in pixels (16, 32, or 64).'),
129
- pixooOutputDir: z.string().optional().describe('Auto-save directory for preview PNG/GIF files.'),
130
+ pixooOutputDir: z.string().transform((dir) => path.resolve(dir)).optional()
131
+ .describe('Auto-save directory for preview PNG/GIF files; a relative path resolves against the launch directory.'),
130
132
  pixooPushMinIntervalMs: z.coerce.number().int().min(0).default(1000)
131
133
  .describe('Pacing floor between device pushes in milliseconds.'),
132
134
  });
@@ -164,12 +166,11 @@ Handlers receive a unified `ctx` object. Key properties used by this server:
164
166
  |:---------|:------------|
165
167
  | `ctx.log` | Request-scoped logger — `.debug()`, `.info()`, `.notice()`, `.warning()`, `.error()`. Auto-correlates requestId, traceId, tenantId. Dual-sink: Pino **and** `notifications/message` to the client, so treat it as client-visible. |
166
168
  | `ctx.enrich` | Success-path agent context — `.notice()` / `.total()` / `.echo()` / `.truncated()`. Lands only when the definition declares an `enrichment` block. |
167
- | `ctx.fail` | Typed throw against the definition's `errors[]` reason union — auto-populates `data.reason`. |
168
- | `ctx.recoveryFor` | `{ recovery: { hint } }` for a declared reason, resolved from the contract. Pass it as `ctx.fail`'s data argument (or spread it in) to put the declared hint on the wire. |
169
+ | `ctx.fail` | Typed throw against the definition's `errors[]` reason union — auto-populates `data.reason`; the framework fills in the entry's `recovery` as `data.recovery.hint`. |
169
170
  | `ctx.state` | Tenant-scoped KV — `.get`, `.set(key, value, { ttl? })`, `.delete`, `.getMany`, `.list`. Accepts any JSON-serializable value; reads return its JSON form (a `Date` comes back as an ISO string). Keys are validated; colons are not legal separators. |
170
171
  | `ctx.requestInput` / `ctx.inputs` | Multi-round-trip input. `return ctx.requestInput(...)`; read the answers with `ctx.inputs.accepted(key, schema)` on re-entry. Unused by this server. |
171
172
  | `ctx.signal` | `AbortSignal` for cancellation. Pass it to any long-running I/O. |
172
- | `ctx.requestId` | Unique request ID. |
173
+ | `ctx.requestId` | Request ID — the one every log record of the call carries and its error envelope returns as `data.requestId`. |
173
174
  | `ctx.tenantId` | Tenant ID from JWT; `'default'` for stdio or HTTP with auth off. |
174
175
 
175
176
  ---
@@ -192,7 +193,7 @@ Pixoo-specific error reasons declared on tools:
192
193
  | `unknown_icon` | `InvalidParams` | Icon name not in registry | — |
193
194
  | `discovery_failed` | `ServiceUnavailable` | Divoom cloud unreachable | `true` |
194
195
 
195
- `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.
196
+ `PixooService` and `src/renderer/` raise the device, configuration, and asset reasons themselves (a factory error carrying `data.reason`), so those entries carry `thrownBy: 'service'` — lint-only metadata that keeps `error-contract-unthrown` from reading them as dead. A reason the handler throws with `ctx.fail` stays unmarked. Either way, the framework puts the entry's `recovery` on the wire when a failure carrying that `reason` arrives without a hint — a bare `ctx.fail('reason', message)` or a service throw with `data: { reason }` — as `data.recovery.hint`, mirrored into `content[]` unless the message already contains it. Pass an explicit `{ recovery: { hint } }` only to override it for one occurrence — a throw-site hint wins over the contract, so wording that holds for every occurrence of a reason lives in its `errors[]` entry, not at the throw. Every error envelope also carries `data.requestId`, and `content[]` closes with `(reason … · request <id>)`.
196
197
 
197
198
  A contract's `retryable` reaches the wire only through `ctx.fail`; a service throw carries it only when the service writes `data.retryable` itself. `classifyDeviceFailure` (in `pixoo-service.ts`) is the one place a failed device call becomes a reason and a retryability — `PixooService` writes both on its push-path throws, and `pixoo_overlay_text` passes the retryability into `ctx.fail` so it overrides the contract default per occurrence.
198
199
 
@@ -212,7 +213,7 @@ errors: [
212
213
  ],
213
214
 
214
215
  // in the handler
215
- throw ctx.fail('invalid_color', `Invalid color "${input.color}"`, ctx.recoveryFor('invalid_color'));
216
+ throw ctx.fail('invalid_color', `Invalid color "${input.color}"`);
216
217
  ```
217
218
 
218
219
  Baseline codes (`InternalError`, `ServiceUnavailable`, `Timeout`, `ValidationError`, `SerializationError`, `RequestCancelled`) bubble freely and don't need declaring. A tool argument that fails the input schema reaches the client as `InvalidParams` (-32602) with `structuredContent.error` — assert that code, not `ValidationError`, in tests.
@@ -240,6 +241,7 @@ src/
240
241
  remote-image.ts # https image fetch into memory for the toolkit loader; stops on ctx.signal
241
242
  mcp-server/
242
243
  tools/
244
+ asset-source-schema.ts # Shared image `source` / sprite `path` schemas: absolute path or URL only
243
245
  device-push.ts # Shared post-render push: preview kept on failure, visibility notice
244
246
  finish-schema.ts # Shared `finish` input schema (colors | palette, plus dither)
245
247
  tools/definitions/
@@ -257,6 +259,8 @@ src/
257
259
  pixoo-design-guide.resource.ts
258
260
  tests/
259
261
  index.session-mode.test.ts # Boots the entry point over HTTP, pins the declared session mode
262
+ config/ # Server config parsed from env vars
263
+ helpers/ # Shared fixtures and assertions (image sources, canvas ink, device failures)
260
264
  renderer/ # Pure renderer unit tests (no device)
261
265
  resources/ # Resource handler tests
262
266
  services/pixoo/ # PixooService tests with a fake client
@@ -378,7 +382,7 @@ Each per-version file opens with YAML frontmatter: `summary` (required, ≤350 c
378
382
  ## Checklist
379
383
 
380
384
  - [ ] Zod schemas: all fields have `.describe()`, only JSON-Schema-serializable types (no `z.custom()`, `z.date()`, `z.transform()`, `z.bigint()`, `z.symbol()`, `z.void()`, `z.map()`, `z.set()`, `z.function()`, `z.nan()`)
381
- - [ ] Tool inputs are strict at the root — an undeclared argument key is rejected by name. Add `.passthrough()` / `.catchall()` only where an open object is genuinely required
385
+ - [ ] Tool inputs are closed at every level — the framework closes the root `z.object`, and every nested input object is a `z.strictObject`, so an undeclared key fails by its full path (`Unknown key style.colour. style accepts: …`) instead of being dropped. A tool with nested input objects gets a schema test that walks its `input` and fails on any open object below the root (`tests/helpers/zod-object-nodes.ts`). Use `z.looseObject()` / `.catchall()` only where an open object is genuinely required
382
386
  - [ ] JSDoc `@fileoverview` + `@module` on every file
383
387
  - [ ] `ctx.log` for logging, `ctx.state` for storage
384
388
  - [ ] Handlers throw on failure — error factories or plain `Error`, no try/catch
package/CLAUDE.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # Developer Protocol
2
2
 
3
3
  **Server:** pixoo-mcp-server
4
- **Version:** 1.2.2
5
- **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.9`
4
+ **Version:** 1.3.0
5
+ **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.10`
6
6
  **Engines:** Bun ≥1.4.0, Node ≥24.0.0
7
7
  **MCP SDK:** `@modelcontextprotocol/server` ^2.1.0 (protocol revision 2026-07-28 alongside the 2025 era)
8
8
  **Zod:** ^4.6.5
@@ -118,6 +118,7 @@ export const pixooDeviceStatusResource = resource('pixoo://device/status', {
118
118
 
119
119
  ```ts
120
120
  // src/config/server-config.ts
121
+ import * as path from 'node:path';
121
122
  import { z } from '@cyanheads/mcp-ts-core';
122
123
  import { parseEnvConfig } from '@cyanheads/mcp-ts-core/config';
123
124
 
@@ -126,7 +127,8 @@ const ServerConfigSchema = z.object({
126
127
  pixooSize: z.coerce.number()
127
128
  .refine((v) => v === 16 || v === 32 || v === 64)
128
129
  .default(64).describe('Display size in pixels (16, 32, or 64).'),
129
- pixooOutputDir: z.string().optional().describe('Auto-save directory for preview PNG/GIF files.'),
130
+ pixooOutputDir: z.string().transform((dir) => path.resolve(dir)).optional()
131
+ .describe('Auto-save directory for preview PNG/GIF files; a relative path resolves against the launch directory.'),
130
132
  pixooPushMinIntervalMs: z.coerce.number().int().min(0).default(1000)
131
133
  .describe('Pacing floor between device pushes in milliseconds.'),
132
134
  });
@@ -164,12 +166,11 @@ Handlers receive a unified `ctx` object. Key properties used by this server:
164
166
  |:---------|:------------|
165
167
  | `ctx.log` | Request-scoped logger — `.debug()`, `.info()`, `.notice()`, `.warning()`, `.error()`. Auto-correlates requestId, traceId, tenantId. Dual-sink: Pino **and** `notifications/message` to the client, so treat it as client-visible. |
166
168
  | `ctx.enrich` | Success-path agent context — `.notice()` / `.total()` / `.echo()` / `.truncated()`. Lands only when the definition declares an `enrichment` block. |
167
- | `ctx.fail` | Typed throw against the definition's `errors[]` reason union — auto-populates `data.reason`. |
168
- | `ctx.recoveryFor` | `{ recovery: { hint } }` for a declared reason, resolved from the contract. Pass it as `ctx.fail`'s data argument (or spread it in) to put the declared hint on the wire. |
169
+ | `ctx.fail` | Typed throw against the definition's `errors[]` reason union — auto-populates `data.reason`; the framework fills in the entry's `recovery` as `data.recovery.hint`. |
169
170
  | `ctx.state` | Tenant-scoped KV — `.get`, `.set(key, value, { ttl? })`, `.delete`, `.getMany`, `.list`. Accepts any JSON-serializable value; reads return its JSON form (a `Date` comes back as an ISO string). Keys are validated; colons are not legal separators. |
170
171
  | `ctx.requestInput` / `ctx.inputs` | Multi-round-trip input. `return ctx.requestInput(...)`; read the answers with `ctx.inputs.accepted(key, schema)` on re-entry. Unused by this server. |
171
172
  | `ctx.signal` | `AbortSignal` for cancellation. Pass it to any long-running I/O. |
172
- | `ctx.requestId` | Unique request ID. |
173
+ | `ctx.requestId` | Request ID — the one every log record of the call carries and its error envelope returns as `data.requestId`. |
173
174
  | `ctx.tenantId` | Tenant ID from JWT; `'default'` for stdio or HTTP with auth off. |
174
175
 
175
176
  ---
@@ -192,7 +193,7 @@ Pixoo-specific error reasons declared on tools:
192
193
  | `unknown_icon` | `InvalidParams` | Icon name not in registry | — |
193
194
  | `discovery_failed` | `ServiceUnavailable` | Divoom cloud unreachable | `true` |
194
195
 
195
- `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.
196
+ `PixooService` and `src/renderer/` raise the device, configuration, and asset reasons themselves (a factory error carrying `data.reason`), so those entries carry `thrownBy: 'service'` — lint-only metadata that keeps `error-contract-unthrown` from reading them as dead. A reason the handler throws with `ctx.fail` stays unmarked. Either way, the framework puts the entry's `recovery` on the wire when a failure carrying that `reason` arrives without a hint — a bare `ctx.fail('reason', message)` or a service throw with `data: { reason }` — as `data.recovery.hint`, mirrored into `content[]` unless the message already contains it. Pass an explicit `{ recovery: { hint } }` only to override it for one occurrence — a throw-site hint wins over the contract, so wording that holds for every occurrence of a reason lives in its `errors[]` entry, not at the throw. Every error envelope also carries `data.requestId`, and `content[]` closes with `(reason … · request <id>)`.
196
197
 
197
198
  A contract's `retryable` reaches the wire only through `ctx.fail`; a service throw carries it only when the service writes `data.retryable` itself. `classifyDeviceFailure` (in `pixoo-service.ts`) is the one place a failed device call becomes a reason and a retryability — `PixooService` writes both on its push-path throws, and `pixoo_overlay_text` passes the retryability into `ctx.fail` so it overrides the contract default per occurrence.
198
199
 
@@ -212,7 +213,7 @@ errors: [
212
213
  ],
213
214
 
214
215
  // in the handler
215
- throw ctx.fail('invalid_color', `Invalid color "${input.color}"`, ctx.recoveryFor('invalid_color'));
216
+ throw ctx.fail('invalid_color', `Invalid color "${input.color}"`);
216
217
  ```
217
218
 
218
219
  Baseline codes (`InternalError`, `ServiceUnavailable`, `Timeout`, `ValidationError`, `SerializationError`, `RequestCancelled`) bubble freely and don't need declaring. A tool argument that fails the input schema reaches the client as `InvalidParams` (-32602) with `structuredContent.error` — assert that code, not `ValidationError`, in tests.
@@ -240,6 +241,7 @@ src/
240
241
  remote-image.ts # https image fetch into memory for the toolkit loader; stops on ctx.signal
241
242
  mcp-server/
242
243
  tools/
244
+ asset-source-schema.ts # Shared image `source` / sprite `path` schemas: absolute path or URL only
243
245
  device-push.ts # Shared post-render push: preview kept on failure, visibility notice
244
246
  finish-schema.ts # Shared `finish` input schema (colors | palette, plus dither)
245
247
  tools/definitions/
@@ -257,6 +259,8 @@ src/
257
259
  pixoo-design-guide.resource.ts
258
260
  tests/
259
261
  index.session-mode.test.ts # Boots the entry point over HTTP, pins the declared session mode
262
+ config/ # Server config parsed from env vars
263
+ helpers/ # Shared fixtures and assertions (image sources, canvas ink, device failures)
260
264
  renderer/ # Pure renderer unit tests (no device)
261
265
  resources/ # Resource handler tests
262
266
  services/pixoo/ # PixooService tests with a fake client
@@ -378,7 +382,7 @@ Each per-version file opens with YAML frontmatter: `summary` (required, ≤350 c
378
382
  ## Checklist
379
383
 
380
384
  - [ ] Zod schemas: all fields have `.describe()`, only JSON-Schema-serializable types (no `z.custom()`, `z.date()`, `z.transform()`, `z.bigint()`, `z.symbol()`, `z.void()`, `z.map()`, `z.set()`, `z.function()`, `z.nan()`)
381
- - [ ] Tool inputs are strict at the root — an undeclared argument key is rejected by name. Add `.passthrough()` / `.catchall()` only where an open object is genuinely required
385
+ - [ ] Tool inputs are closed at every level — the framework closes the root `z.object`, and every nested input object is a `z.strictObject`, so an undeclared key fails by its full path (`Unknown key style.colour. style accepts: …`) instead of being dropped. A tool with nested input objects gets a schema test that walks its `input` and fails on any open object below the root (`tests/helpers/zod-object-nodes.ts`). Use `z.looseObject()` / `.catchall()` only where an open object is genuinely required
382
386
  - [ ] JSDoc `@fileoverview` + `@module` on every file
383
387
  - [ ] `ctx.log` for logging, `ctx.state` for storage
384
388
  - [ ] Handlers throw on failure — error factories or plain `Error`, no try/catch
package/Dockerfile CHANGED
@@ -29,80 +29,97 @@ RUN bun run build
29
29
 
30
30
 
31
31
  # ==============================================================================
32
- # Production Stage
32
+ # Production Dependencies Stage
33
33
  #
34
- # This stage creates a minimal, optimized, and secure image for running the
35
- # application. It uses a slim base image and only includes production
36
- # dependencies and build artifacts.
34
+ # Installs the production dependency tree for the target platform. Every step
35
+ # here can run JavaScript — bunfig.toml's security scanner runs as a Bun
36
+ # program, and so does the OTel script — so the stage runs on $BUILDPLATFORM
37
+ # and cross-installs with `--os`/`--cpu`, which pick each platform-specific
38
+ # optional dependency (sharp's native `@img/*` bindings) for the target. Only
39
+ # `node_modules` leaves this stage.
40
+ #
41
+ # A clean image rather than `FROM build`: the build stage's node_modules holds
42
+ # devDependencies.
37
43
  # ==============================================================================
38
- FROM oven/bun:1.4.2-slim AS production
44
+ FROM --platform=$BUILDPLATFORM oven/bun:1.4.2 AS deps
39
45
 
40
46
  WORKDIR /usr/src/app
41
47
 
42
- # Set the environment to production for performance and to ensure only
43
- # production dependencies are installed.
44
- ENV NODE_ENV=production
45
-
46
- # OCI image metadata (https://github.com/opencontainers/image-spec/blob/main/annotations.md)
47
- ARG APP_VERSION
48
- LABEL org.opencontainers.image.title="pixoo-mcp-server"
49
- LABEL org.opencontainers.image.description="Render and push styled pixel art, text, dashboards, and animations to Divoom Pixoo LED displays."
50
- LABEL org.opencontainers.image.licenses="Apache-2.0"
51
- LABEL org.opencontainers.image.version="${APP_VERSION}"
52
- LABEL org.opencontainers.image.source="https://github.com/cyanheads/pixoo-mcp-server"
53
-
54
48
  # Copy dependency manifests. `bunfig.toml` rides along so every install below
55
49
  # passes its release-age gate and security scanner, as a local install does.
56
50
  COPY package.json bun.lock bunfig.toml ./
57
51
 
58
52
  # The scanner bunfig.toml names is a devDependency, and Bun installs a missing
59
53
  # scanner through the same production-filtered install, which omits it and
60
- # aborts. Seed it from the build stage's full install instead. Remove this line
61
- # together with the scanner if bunfig.toml stops naming one.
54
+ # aborts. Seed it from the build stage's full install instead. Remove this line,
55
+ # and the `rm` at the end of this stage, if bunfig.toml stops naming a scanner.
62
56
  COPY --from=build /usr/src/app/node_modules/@socketsecurity/bun-security-scanner ./node_modules/@socketsecurity/bun-security-scanner
63
57
 
58
+ # Docker names the target architecture `amd64`/`arm64`; Bun's `--cpu` takes
59
+ # `x64`/`arm64`. Mapped once here, read by both installs below. `oven/bun`
60
+ # publishes only these two architectures, so any other target fails here.
61
+ ARG TARGETOS
62
+ ARG TARGETARCH
63
+ RUN case "$TARGETARCH" in \
64
+ amd64) echo x64 ;; \
65
+ arm64) echo arm64 ;; \
66
+ *) echo "Unsupported TARGETARCH '$TARGETARCH': expected amd64 or arm64" >&2; exit 1 ;; \
67
+ esac > .bun-cpu
68
+
64
69
  # Install only production dependencies, ignoring any lifecycle scripts (like 'prepare')
65
70
  # that are not needed in the final production image.
66
71
  # `--omit=peer` drops the framework's optional peer tiers (test runner, service
67
72
  # SDKs, parsers) that Bun would otherwise auto-install. Anything this server
68
73
  # actually imports belongs in its own `dependencies`, so nothing needed at
69
- # runtime is lost. The OTEL step below carries the same flag — without it, that
70
- # install re-resolves the graph and pulls every optional peer back in.
74
+ # runtime is lost.
71
75
  RUN --mount=type=cache,target=/root/.bun/install/cache \
72
- bun install --production --omit=peer --frozen-lockfile --ignore-scripts
76
+ bun install --production --omit=peer --frozen-lockfile --ignore-scripts \
77
+ --os="$TARGETOS" --cpu="$(cat .bun-cpu)"
73
78
 
74
79
  # Conditionally install OpenTelemetry optional peer dependencies (Tier 3).
75
80
  # Installed by default. Omit them for a leaner image at build time
76
81
  # with: docker build --build-arg OTEL_ENABLED=false
77
- # Each package is requested at the range the installed framework declares in
78
- # `peerDependencies`, so the resolution stays inside the framework's tested
79
- # peer range; a name with no declared range fails the build.
82
+ # The script reads the list and each range from the installed framework's
83
+ # `peerDependencies` and passes the target flags on to its `bun install`.
84
+ COPY scripts/install-otel.ts ./scripts/
80
85
  ARG OTEL_ENABLED=true
81
86
  RUN --mount=type=cache,target=/root/.bun/install/cache \
82
87
  if [ "$OTEL_ENABLED" = "true" ]; then \
83
- specs=$(bun -e ' \
84
- const { peerDependencies: peers } = await Bun.file("node_modules/@cyanheads/mcp-ts-core/package.json").json(); \
85
- const names = process.argv.slice(1); \
86
- const missing = names.filter((name) => !peers?.[name]); \
87
- if (missing.length > 0) throw new Error(`no peerDependencies range for ${missing.join(", ")}`); \
88
- console.log(names.map((name) => `${name}@${peers[name]}`).join(" ")); \
89
- ' \
90
- @hono/otel \
91
- @opentelemetry/api-logs \
92
- @opentelemetry/exporter-logs-otlp-http \
93
- @opentelemetry/exporter-metrics-otlp-http \
94
- @opentelemetry/exporter-trace-otlp-http \
95
- @opentelemetry/instrumentation-http \
96
- @opentelemetry/instrumentation-pino \
97
- @opentelemetry/resources \
98
- @opentelemetry/sdk-logs \
99
- @opentelemetry/sdk-metrics \
100
- @opentelemetry/sdk-node \
101
- @opentelemetry/sdk-trace-node \
102
- @opentelemetry/semantic-conventions) \
103
- && bun add --omit=dev --omit=peer --ignore-scripts $specs; \
88
+ bun scripts/install-otel.ts --os="$TARGETOS" --cpu="$(cat .bun-cpu)"; \
104
89
  fi
105
90
 
91
+ # The seeded scanner served only the installs above; keep it out of the image.
92
+ RUN rm -rf node_modules/@socketsecurity/bun-security-scanner
93
+
94
+
95
+ # ==============================================================================
96
+ # Production Stage
97
+ #
98
+ # This stage creates a minimal, optimized, and secure image for running the
99
+ # application. It uses a slim base image and only includes production
100
+ # dependencies and build artifacts. Its only Bun invocations are HEALTHCHECK
101
+ # and CMD, which run on the real target at container start.
102
+ # ==============================================================================
103
+ FROM oven/bun:1.4.2-slim AS production
104
+
105
+ WORKDIR /usr/src/app
106
+
107
+ # Set the environment to production for performance.
108
+ ENV NODE_ENV=production
109
+
110
+ # OCI image metadata (https://github.com/opencontainers/image-spec/blob/main/annotations.md)
111
+ ARG APP_VERSION
112
+ LABEL org.opencontainers.image.title="pixoo-mcp-server"
113
+ LABEL org.opencontainers.image.description="Render and push styled pixel art, text, dashboards, and animations to Divoom Pixoo LED displays."
114
+ LABEL org.opencontainers.image.licenses="Apache-2.0"
115
+ LABEL org.opencontainers.image.version="${APP_VERSION}"
116
+ LABEL org.opencontainers.image.source="https://github.com/cyanheads/pixoo-mcp-server"
117
+
118
+ # The manifest comes from the build context: the deps stage's copy was rewritten
119
+ # by the OTel install, and the runtime reads only its name, version, and type.
120
+ COPY package.json ./
121
+ COPY --from=deps /usr/src/app/node_modules ./node_modules
122
+
106
123
  # Copy the compiled application code from the build stage
107
124
  COPY --from=build /usr/src/app/dist ./dist
108
125
 
package/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
  <div align="center">
9
9
 
10
- [![Version](https://img.shields.io/badge/Version-1.2.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.1.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.2-blueviolet.svg?style=flat-square)](https://bun.sh/)
10
+ [![Version](https://img.shields.io/badge/Version-1.3.0-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.1.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.2-blueviolet.svg?style=flat-square)](https://bun.sh/)
11
11
 
12
12
  </div>
13
13
 
@@ -53,8 +53,8 @@ Tools cover the same ground for tool-only clients: `pixoo_control_device` reads
53
53
  ### `pixoo_display_text` <sub>tool</sub>
54
54
 
55
55
  - `text` as a string or an array of lines; `theme` (`midnight`, `ember`, `claude`, `ice`, `neon`, `forest`, `mono`) sets the background and default palette. `style` takes a `palette` ramp (`ember`, `ice`, `neon`, `fire`, `lavender`, `claude`, `mono`) or a custom `{ from, to }`, plus `shadow`, `outline`, and `scale` 1–8; `position` is semantic or in pixels, and `align` lines up multi-line text
56
- - `font`: `standard` (5×7) and `compact` (3×5) draw printable ASCII plus `° ← ↑ → ↓ ▲ ▼ ♥ · …`; `numerals` is an 11×18 digit face for clocks and readouts that draws 0–9, space, and `: . - + / % ° ?`, and text holding any other character fails validation, naming those characters
57
- - `layout[]` reports every fit decision as an `action` (`none`, `shrunk-to-compact`, or `scrolling`), the `font` used, and whether each line's box `fits` on the panel; a scrolling line's box starts at x 0, where its static frame draws it. Single-line text falls back from the standard to the compact font unless `font` is set — never to `numerals` — and text still too wide only scrolls under `effect: "auto"` or `"scroll"`
56
+ - `font`: `standard` (5×7) and `compact` (3×5) draw printable ASCII plus `° ← ↑ → ↓ ▲ ▼ ♥ · …`, and any other character (`€`, `’`, a newline inside one string) as `?`, named with its code point and line index in the response `notice`; `numerals` is an 11×18 digit face for clocks and readouts that draws 0–9, space, and `: . - + / % ° ?`, and text holding any other character fails validation, naming those characters
57
+ - `layout[]` reports every fit decision as an `action` (`shrunk-to-compact` when the text fits only in the compact font, `scrolling` when the returned frames scroll, `none` otherwise), the `font` used, and whether each line's box `fits` on the panel; a single line too wide for the panel starts at x 0, cut at the right edge when it does not scroll. Single-line text falls back from the standard to the compact font unless `font` is set — never to `numerals` — and text still too wide only scrolls under `effect: "auto"` or `"scroll"`
58
58
  - `effect`: `scroll` makes one pass in up to 40 frames, `auto` scrolls only on overflow, and `float` and `pulse` loop over 20 frames; `frames` reports the count, and the animation pushes as one device animation
59
59
 
60
60
  ---
@@ -63,8 +63,8 @@ Tools cover the same ground for tool-only clients: `pixoo_control_device` reads
63
63
 
64
64
  - Up to 50 `elements` drawn back-to-front: `text` (in the same three fonts as `pixoo_display_text`), `icon`, `rect`, `circle`, `line`, `progress`, `sparkline`, `bitmap`, `pixels`, `image` (absolute path or https URL, with the same `finish` as `pixoo_push_image`), `sprite` (absolute path). The `background` is a solid color, a `v` / `h` / `r` gradient, or a `theme`
65
65
  - Every element takes `opacity` (each pixel lands at its own alpha × `opacity`, so soft edges fade evenly) and `blend`: `normal`, `add` (glows and light beams), `screen`, or `multiply`. `line` and outline `circle` take `strokeWidth` and `antialias`, and a `rect` border takes `strokeWidth`, growing inward; either field on a shape that draws no stroke fails validation, naming it
66
- - Returns `layout[]`: each element's placed box — for a wide or anti-aliased stroke, every pixel it draws — and whether it `fits` on the panel. An absolute `output` path saves the first frame as a PNG in place of the `PIXOO_OUTPUT_DIR` auto-save. Typed failures: `asset_not_found`, `invalid_image` (an image or sprite that was read but does not decode), `invalid_color`, `unknown_icon`, `invalid_output_path`
67
- - Animation through per-element `effect` presets (`float`, `scroll-left`, `scroll-right`, `pulse`, `blink`, `twinkle`, `drift`, `fade-in`, `fade-out`) or raw `animate` keyframes over `dx`, `dy`, `opacity` (numbers or numeric strings), `visible` (`true`/`false`), and `color` (interpolated through RGB on any element with a `color`), each track holding at least one keyframe; `frames` 1–40, `speed` 10–2000 ms per frame (default 150). An effect's `amplitude` sets the movement of `float`, `scroll-*`, and `drift`, and the 0–1 depth of the `pulse` and `twinkle` opacity dip
66
+ - Returns `layout[]`: each element's placed box — for a wide or anti-aliased stroke, every pixel it draws — and whether it `fits` on the panel. Elements are placed as given and never refit, so `action` is always `none`. An absolute `output` path saves the first frame as a PNG in place of the `PIXOO_OUTPUT_DIR` auto-save. Typed failures: `asset_not_found`, `invalid_image` (an image or sprite that was read but does not decode), `invalid_color`, `unknown_icon`, `invalid_output_path`
67
+ - Animation through per-element `effect` presets (`float`, `scroll-left`, `scroll-right`, `pulse`, `blink`, `twinkle`, `drift`, `fade-in`, `fade-out`) or raw `animate` keyframes over `dx`, `dy`, `opacity` (numbers or numeric strings), `visible` (`true`/`false`), and `color` (interpolated through RGB on any element with a `color`), each track holding at least one keyframe; `frames` 1–40, `speed` 10–2000 ms per frame (default 150). An element takes `effect` or `animate`, not both. An effect's `amplitude` sets the movement of `float`, `scroll-*`, and `drift`, and the 0–1 depth of the `pulse` and `twinkle` opacity dip
68
68
 
69
69
  ---
70
70
 
@@ -144,7 +144,7 @@ Pixoo-specific:
144
144
  Agent-friendly output:
145
145
 
146
146
  - Preview on every render: `pixoo_display_text`, `pixoo_compose_scene`, and `pixoo_push_image` return the frame as an 8× upscaled PNG image block, pushed or not, so `push: false` checks a design with no device attached. Animations preview as a grid of every frame, since GIF display varies across MCP clients; the GIF itself is saved to `PIXOO_OUTPUT_DIR` when set
147
- - Layout transparency: `layout[]` reports every renderer decision (font fallback, truncation, scrolling, clipping) so agents can refine a design
147
+ - Layout transparency: `layout[]` reports every renderer decision (font fallback, scrolling, and whether each box fits on the panel) so agents can refine a design
148
148
  - Device truth: `pushed` reflects the device ACK, and the `deviceState` read back after a push comes with a notice naming the fix when the render won't be visible (screen off, brightness ≤ 10, off the custom channel)
149
149
  - Renders survive failed pushes: the typed error (`device_unreachable`, `device_http_error`, `device_rejected`, `no_device_configured`) carries `outputFiles` pointing at the saved preview (the `PIXOO_OUTPUT_DIR` copy, or a temp file when that is unset)
150
150
 
@@ -252,7 +252,7 @@ cp .env.example .env
252
252
  |:---|:---|:---|
253
253
  | `PIXOO_IP` | Device IP on the local network. **Required** for pushes, overlays, and device control; discovery, design briefs, and `push: false` renders work without it. | — |
254
254
  | `PIXOO_SIZE` | Display size in pixels: `16`, `32`, or `64`. | `64` |
255
- | `PIXOO_OUTPUT_DIR` | Directory where render tools save preview PNG and GIF files. Unset, previews are returned only in the response. | — |
255
+ | `PIXOO_OUTPUT_DIR` | Directory where render tools save preview PNG and GIF files. A relative path resolves against the directory the server was launched from, so the saved paths it reports are absolute. Unset, previews are returned only in the response. | — |
256
256
  | `PIXOO_PUSH_MIN_INTERVAL_MS` | Minimum gap between device pushes, in ms. | `1000` |
257
257
  | `MCP_TRANSPORT_TYPE` | Transport: `stdio` or `http`. | `stdio` |
258
258
  | `MCP_HTTP_PORT` | HTTP server port. | `3010` |
@@ -0,0 +1,35 @@
1
+ ---
2
+ summary: "Stricter tool inputs: unknown nested keys, relative image paths, and effect with animate now fail -32602 instead of being dropped or resolved silently. layout[].action reports only what the frames do, standard and compact text names the characters it draws as ?, and errors carry the request ID."
3
+ breaking: true
4
+ security: false
5
+ ---
6
+
7
+ # 1.3.0 — 2026-09-26
8
+
9
+ ## Added
10
+
11
+ - **Fallback-glyph `notice` on `pixoo_display_text` and `pixoo_compose_scene`** ([#57](https://github.com/cyanheads/pixoo-mcp-server/issues/57)). A `standard` or `compact` line, scene `text` element, or `progress` `label` holding a character those fonts draw as `?` names it by element and code point (`element 1 "×" (U+00D7)`); the render is unchanged.
12
+
13
+ ## Changed
14
+
15
+ - **Image `source` and sprite `path` must be an absolute local path or a URL** ([#53](https://github.com/cyanheads/pixoo-mcp-server/issues/53)). A relative or `~` path, a `file://` URL, or `""` now fails `-32602` `invalid_arguments`: `source: Must be an absolute local path or an https URL, not a relative path, a ~ path, or a file:// URL.` (in a scene, `elements.<i>.source` / `elements.<i>.path`).
16
+ - **Every nested `pixoo_compose_scene` input object rejects unknown keys** ([#54](https://github.com/cyanheads/pixoo-mcp-server/issues/54)) — elements, `style` and its palette stop, `effect`, `animate`, rect `gradient`, `pixels` points, `background` — failing `-32602` by full path: `Unknown key elements.0.colour. elements.0 accepts: type, x, y, w, h, color, …`.
17
+ - **`animate` takes only the tracks an element drives** (#54): an unknown track fails as `Unknown key elements.0.animate.y. …`, and `bitmap`, `progress`, `image`, and `sprite` reject a `color` track.
18
+ - **A compose `background` object holds exactly one of `gradient` or `theme`** (#54). Both or neither fails `background: Set exactly one of gradient or theme.` instead of the gradient winning.
19
+ - **A scene element setting both `effect` and `animate` fails** ([#59](https://github.com/cyanheads/pixoo-mcp-server/issues/59)) with `elements.<i>.effect: Set either effect or animate on an element, not both: …`, instead of dropping the effect.
20
+ - **`pixoo_display_text` rejects unknown keys in `style`, its palette stop, `position`, and `background`** ([#60](https://github.com/cyanheads/pixoo-mcp-server/issues/60)): `Unknown key style.colour. style accepts: palette, shadow, outline, scale, color.`
21
+ - **`layout[].action` reports what the returned frames do** ([#56](https://github.com/cyanheads/pixoo-mcp-server/issues/56)). A `pixoo_display_text` line too wide to fit that does not scroll reports `none` with `fits: false`, not `scrolling`; the output schemas advertise `none`/`shrunk-to-compact`/`scrolling` (display text) and `none` (compose), and `element` is always a number.
22
+ - **Error results carry the request ID** (framework): `data.requestId`, with the text closing `(reason … · request <id>)`.
23
+ - **Input-validation rejections log at `notice`** instead of `error` (framework), tagged `mcp.error.severity: "notice"` on `mcp.errors.classified`.
24
+
25
+ ## Fixed
26
+
27
+ - **A relative `PIXOO_OUTPUT_DIR` reports absolute `outputFiles`** ([#61](https://github.com/cyanheads/pixoo-mcp-server/issues/61)); it resolves against the launch directory when the config is read, so files land where they did.
28
+ - **Image URL schemes are read case-insensitively** (#53), so an `HTTPS://…` source is fetched rather than read as a local path.
29
+ - **`pixoo_discover_devices` declares the `discovery_failed` recovery hint callers receive**; the wording on the wire is unchanged.
30
+ - **Multi-arch Docker builds no longer run Bun under emulation**: production dependencies cross-install in a build-platform `deps` stage, and `lint:packaging` fails a target-platform stage that runs JavaScript.
31
+
32
+ ## Dependencies
33
+
34
+ - `@cyanheads/mcp-ts-core` `^0.13.9` → `^0.13.10`
35
+ - `hono` `4.13.8` → `4.13.9` (transitive, via the framework)
@@ -6,7 +6,7 @@ import { z } from '@cyanheads/mcp-ts-core';
6
6
  declare const ServerConfigSchema: z.ZodObject<{
7
7
  pixooIp: z.ZodOptional<z.ZodString>;
8
8
  pixooSize: z.ZodDefault<z.ZodCoercedNumber<unknown> & z.ZodType<16 | 32 | 64, unknown, z.core.$ZodTypeInternals<16 | 32 | 64, unknown>>>;
9
- pixooOutputDir: z.ZodOptional<z.ZodString>;
9
+ pixooOutputDir: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>;
10
10
  pixooPushMinIntervalMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
11
11
  }, z.core.$strip>;
12
12
  export type ServerConfig = z.infer<typeof ServerConfigSchema>;
@@ -1 +1 @@
1
- {"version":3,"file":"server-config.d.ts","sourceRoot":"","sources":["../../src/config/server-config.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,wBAAwB,CAAC;AAG3C,QAAA,MAAM,kBAAkB;;;;;iBAgBtB,CAAC;AAEH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAI9D,wBAAgB,eAAe,IAAI,YAAY,CAQ9C;AAED,yCAAyC;AACzC,wBAAgB,iBAAiB,IAAI,IAAI,CAExC"}
1
+ {"version":3,"file":"server-config.d.ts","sourceRoot":"","sources":["../../src/config/server-config.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAE,CAAC,EAAE,MAAM,wBAAwB,CAAC;AAG3C,QAAA,MAAM,kBAAkB;;;;;iBAsBtB,CAAC;AAEH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAI9D,wBAAgB,eAAe,IAAI,YAAY,CAQ9C;AAED,yCAAyC;AACzC,wBAAgB,iBAAiB,IAAI,IAAI,CAExC"}
@@ -2,6 +2,7 @@
2
2
  * @fileoverview Server-specific environment variable configuration for pixoo-mcp-server.
3
3
  * @module config/server-config
4
4
  */
5
+ import * as path from 'node:path';
5
6
  import { z } from '@cyanheads/mcp-ts-core';
6
7
  import { parseEnvConfig } from '@cyanheads/mcp-ts-core/config';
7
8
  const ServerConfigSchema = z.object({
@@ -13,7 +14,11 @@ const ServerConfigSchema = z.object({
13
14
  })
14
15
  .default(64)
15
16
  .describe('Display size in pixels (16, 32, or 64).'),
16
- pixooOutputDir: z.string().optional().describe('Auto-save directory for preview PNG/GIF files.'),
17
+ pixooOutputDir: z
18
+ .string()
19
+ .transform((dir) => path.resolve(dir))
20
+ .optional()
21
+ .describe('Auto-save directory for preview PNG/GIF files. A relative path resolves against the working directory the server was launched from, so every saved path it reports is absolute.'),
17
22
  pixooPushMinIntervalMs: z.coerce
18
23
  .number()
19
24
  .int()
@@ -1 +1 @@
1
- {"version":3,"file":"server-config.js","sourceRoot":"","sources":["../../src/config/server-config.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,wBAAwB,CAAC;AAC3C,OAAO,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAE/D,MAAM,kBAAkB,GAAG,CAAC,CAAC,MAAM,CAAC;IAClC,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,iCAAiC,CAAC;IAC1E,SAAS,EAAE,CAAC,CAAC,MAAM;SAChB,MAAM,EAAE;SACR,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,EAAE;QAC/C,OAAO,EAAE,uBAAuB;KACjC,CAAC;SACD,OAAO,CAAC,EAAE,CAAC;SACX,QAAQ,CAAC,yCAAyC,CAAC;IACtD,cAAc,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,gDAAgD,CAAC;IAChG,sBAAsB,EAAE,CAAC,CAAC,MAAM;SAC7B,MAAM,EAAE;SACR,GAAG,EAAE;SACL,GAAG,CAAC,CAAC,CAAC;SACN,OAAO,CAAC,IAAI,CAAC;SACb,QAAQ,CAAC,qDAAqD,CAAC;CACnE,CAAC,CAAC;AAIH,IAAI,OAAiC,CAAC;AAEtC,MAAM,UAAU,eAAe;IAC7B,OAAO,KAAK,cAAc,CAAC,kBAAkB,EAAE;QAC7C,OAAO,EAAE,UAAU;QACnB,SAAS,EAAE,YAAY;QACvB,cAAc,EAAE,kBAAkB;QAClC,sBAAsB,EAAE,4BAA4B;KACrD,CAAC,CAAC;IACH,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,yCAAyC;AACzC,MAAM,UAAU,iBAAiB;IAC/B,OAAO,GAAG,SAAS,CAAC;AACtB,CAAC"}
1
+ {"version":3,"file":"server-config.js","sourceRoot":"","sources":["../../src/config/server-config.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAClC,OAAO,EAAE,CAAC,EAAE,MAAM,wBAAwB,CAAC;AAC3C,OAAO,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAE/D,MAAM,kBAAkB,GAAG,CAAC,CAAC,MAAM,CAAC;IAClC,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,iCAAiC,CAAC;IAC1E,SAAS,EAAE,CAAC,CAAC,MAAM;SAChB,MAAM,EAAE;SACR,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,EAAE;QAC/C,OAAO,EAAE,uBAAuB;KACjC,CAAC;SACD,OAAO,CAAC,EAAE,CAAC;SACX,QAAQ,CAAC,yCAAyC,CAAC;IACtD,cAAc,EAAE,CAAC;SACd,MAAM,EAAE;SACR,SAAS,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;SACrC,QAAQ,EAAE;SACV,QAAQ,CACP,iLAAiL,CAClL;IACH,sBAAsB,EAAE,CAAC,CAAC,MAAM;SAC7B,MAAM,EAAE;SACR,GAAG,EAAE;SACL,GAAG,CAAC,CAAC,CAAC;SACN,OAAO,CAAC,IAAI,CAAC;SACb,QAAQ,CAAC,qDAAqD,CAAC;CACnE,CAAC,CAAC;AAIH,IAAI,OAAiC,CAAC;AAEtC,MAAM,UAAU,eAAe;IAC7B,OAAO,KAAK,cAAc,CAAC,kBAAkB,EAAE;QAC7C,OAAO,EAAE,UAAU;QACnB,SAAS,EAAE,YAAY;QACvB,cAAc,EAAE,kBAAkB;QAClC,sBAAsB,EAAE,4BAA4B;KACrD,CAAC,CAAC;IACH,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,yCAAyC;AACzC,MAAM,UAAU,iBAAiB;IAC/B,OAAO,GAAG,SAAS,CAAC;AACtB,CAAC"}
@@ -15,7 +15,7 @@ At typical viewing distance for a 64×64 LED display:
15
15
  - **Font choices:**
16
16
  - Standard (5×7): default for all text. Readable at 1× scale. Excellent at 2× (14px tall = chunky block letters)
17
17
  - Compact (3×5): secondary text, captions, labels when space is tight
18
- - Standard and compact both draw printable ASCII plus \`° ← ↑ → ↓ ▲ ▼ ♥ · …\` for units, trends, and separators
18
+ - Standard and compact both draw printable ASCII plus \`° ← ↑ → ↓ ▲ ▼ ♥ · …\` for units, trends, and separators. Any other character (\`€\`, \`×\`, \`’\`, \`—\`, a newline inside one string, the invisible U+FE0F after \`♥\`) draws as \`?\`, and the response notice names it with its code point and element index
19
19
  - Numerals (11×18): clocks and hero readouts. Draws 0–9, space, and \`: . - + / % ° ?\` only, each digit on one 13px advance so a changing value keeps its width. Auto-fit never picks it; text holding any other character is rejected, so units and labels go in their own standard or compact text element
20
20
  - Scale ≥2: decorative or hero use only — verify fit before pushing
21
21
 
@@ -0,0 +1,13 @@
1
+ /**
2
+ * @fileoverview Inputs that name an image asset: the image `source` of pixoo_push_image
3
+ * and of a scene `image` element, and a scene `sprite` element's `path`. Each must be a
4
+ * URL or an absolute local path — a relative path would resolve against the server's
5
+ * working directory, which a caller cannot see.
6
+ * @module mcp-server/tools/asset-source-schema
7
+ */
8
+ import { z } from '@cyanheads/mcp-ts-core';
9
+ /** An image source: an absolute local path or an https URL. */
10
+ export declare const ImageSourceSchema: z.ZodString;
11
+ /** A sprite sheet path: an absolute local path. */
12
+ export declare const SpritePathSchema: z.ZodString;
13
+ //# sourceMappingURL=asset-source-schema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"asset-source-schema.d.ts","sourceRoot":"","sources":["../../../src/mcp-server/tools/asset-source-schema.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,EAAE,CAAC,EAAE,MAAM,wBAAwB,CAAC;AAW3C,+DAA+D;AAC/D,eAAO,MAAM,iBAAiB,aAG5B,CAAC;AAEH,mDAAmD;AACnD,eAAO,MAAM,gBAAgB,aAE3B,CAAC"}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * @fileoverview Inputs that name an image asset: the image `source` of pixoo_push_image
3
+ * and of a scene `image` element, and a scene `sprite` element's `path`. Each must be a
4
+ * URL or an absolute local path — a relative path would resolve against the server's
5
+ * working directory, which a caller cannot see.
6
+ * @module mcp-server/tools/asset-source-schema
7
+ */
8
+ import * as path from 'node:path';
9
+ import { z } from '@cyanheads/mcp-ts-core';
10
+ import { isRemoteSource } from '../../renderer/remote-image.js';
11
+ /**
12
+ * True for a value that names the same asset whatever the server's working directory.
13
+ * An http URL passes: the loaders refuse it as `asset_not_found` with a message naming
14
+ * the https requirement, as they refuse a sprite path given as any URL.
15
+ */
16
+ const namesAssetAbsolutely = (value) => isRemoteSource(value) || path.isAbsolute(value);
17
+ /** An image source: an absolute local path or an https URL. */
18
+ export const ImageSourceSchema = z.string().refine(namesAssetAbsolutely, {
19
+ error: 'Must be an absolute local path or an https URL, not a relative path, a ~ path, or a file:// URL.',
20
+ });
21
+ /** A sprite sheet path: an absolute local path. */
22
+ export const SpritePathSchema = z.string().refine(namesAssetAbsolutely, {
23
+ error: 'Must be an absolute local path, not a relative path, a ~ path, or a file:// URL.',
24
+ });
25
+ //# sourceMappingURL=asset-source-schema.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"asset-source-schema.js","sourceRoot":"","sources":["../../../src/mcp-server/tools/asset-source-schema.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAClC,OAAO,EAAE,CAAC,EAAE,MAAM,wBAAwB,CAAC;AAC3C,OAAO,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAE5D;;;;GAIG;AACH,MAAM,oBAAoB,GAAG,CAAC,KAAa,EAAW,EAAE,CACtD,cAAc,CAAC,KAAK,CAAC,IAAI,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;AAElD,+DAA+D;AAC/D,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,MAAM,CAAC,oBAAoB,EAAE;IACvE,KAAK,EACH,kGAAkG;CACrG,CAAC,CAAC;AAEH,mDAAmD;AACnD,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,MAAM,CAAC,oBAAoB,EAAE;IACtE,KAAK,EAAE,kFAAkF;CAC1F,CAAC,CAAC"}