brs-mcp
by bblietz
README.md
# brs-mcp
> An MCP server that scaffolds runnable Roku channels (BrightScript + SceneGraph) from a validated `AppSpec`, zips them, and optionally sideloads them to a Roku in developer mode.
[](https://nodejs.org)
[](LICENSE)
[](https://modelcontextprotocol.io)
## What it does
An AI assistant (or any MCP client) passes a strict, versioned `AppSpec`. The server returns a complete project tree, produces a sideload-ready zip, and optionally installs it on a Roku device. Every generated file comes from a curated, hand-authored, device-tested template; the server never asks an LLM to write BrightScript.
Same spec in, same bytes out, every time. Zips are byte-reproducible across hosts (sorted entries, fixed mtime). Re-running on a machine in a different time zone produces identical output.
For a fuller styled reference with per-tool I/O examples, see [`docs/index.html`](docs/index.html).
## Install
For a styled walkthrough with prereqs, four numbered steps, and update guidance on one page, see [`docs/install.html`](docs/install.html).
```bash
npm install -g brs-mcp
```
Wire it into your MCP client (e.g., Claude Desktop):
```json
{
"mcpServers": {
"brs-mcp": {
"command": "brs-mcp"
}
}
}
```
The server speaks MCP over stdio. Logs go to stderr; stdout is reserved for JSON-RPC.
### Verify
One-liner that cold-fetches the published package and exercises a real MCP `initialize` handshake. No global install required:
```bash
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"x","version":"1"}}}' | npx -y brs-mcp
```
Expected response on stdout (one JSON object, formatted here for readability):
```json
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2024-11-05",
"capabilities": { "tools": {} },
"serverInfo": { "name": "brs-mcp", "version": "0.1.0" }
}
}
```
Exit code `0`, stderr silent. If you see any stdout output that isn't a single JSON-RPC object, something in your shell is polluting the transport; verify with `npx -y brs-mcp <<< '{}' 2>/dev/null | head -c 200`.
## Tools
### `list_templates`
List every template bundled with this server.
- **Input:** `{}`
- **Output:** `{ "templates": [{ "id", "category", "version", "description" }] }`
### `get_template_schema`
Return the JSON Schema (Draft 7) for a template's `AppSpec`, plus a minimal example.
- **Input:** `{ "id": "video_grid_channel" }`
- **Output:** `{ "schema": <JSON Schema>, "example_spec": <AppSpec> }`
### `generate_app`
Render a Roku channel project from a validated `AppSpec`. Optionally zip and sideload.
- **Input:** `{ "spec", "output_dir", "assets_root"?, "overwrite"?, "zip"?, "sideload"? }`
- **Output (success):** `{ "ok": true, "project_dir", "files_written", "zip_path"?, "sideload"? }`
- `sideload` implies `zip: true` (enforced by schema).
### `package_app`
Zip an already-generated project directory into a sideload-ready archive. Validates a top-level `manifest`. Output is byte-reproducible.
- **Input:** `{ "project_dir", "output_zip"? }`
- **Output:** `{ "ok": true, "zip_path", "size_bytes", "entry_count" }`
### `sideload_app`
Install a zip on a Roku in developer mode via HTTP Digest-authenticated multipart `POST` to `/plugin_install`. **`dev_password` is never logged or echoed.**
- **Input:** `{ "zip_path", "device_ip", "dev_password" }`
- **Output:** `{ "ok": true, "status": "installed" | "identical", "message", "duration_ms", "raw_html"? }`
## Templates
### `screensaver`
Roku screensaver channel. Three styles:
- `slideshow`: crossfade through bundled images.
- `animated`: bouncing colored shapes (no images required).
- `quadrant`: 4-up grid of bundled images with rotating cells.
```json
{
"template": "screensaver",
"spec_version": 1,
"app": { "name": "My Screensaver", "major_version": 1, "minor_version": 0, "build_version": 0 },
"style": "animated"
}
```
> **Roku menu label note:** When sideloaded via the dev web server, screensavers always appear in `Settings -> Theme -> Screensavers` as the literal string `(dev)`, not as the `app.name` value. This is a Roku dev-build UX quirk and applies to every sideloaded screensaver including Roku's own canonical samples. The `screensaver_title` manifest field IS still required (and is presumably used once the screensaver is published through the channel store), but for sideload-based testing, look for the `(dev)` entry.
### `video_grid_channel`
VOD grid: home (`RowList`) → detail → video player. Consumes mRSS, Roku Direct Publisher JSON, or a custom JSON feed.
```json
{
"template": "video_grid_channel",
"spec_version": 1,
"app": { "name": "Example", "major_version": 1, "minor_version": 0, "build_version": 0 },
"branding": {
"primary_color": "#E50914",
"background_color": "#141414",
"text_color": "#FFFFFF",
"splash": { "hd": "./splash_hd.png", "fhd": "./splash_fhd.png" },
"icon": { "hd": "./icon_hd.png", "fhd": "./icon_fhd.png" }
},
"content": {
"feed_url": "https://example.com/feed.json",
"feed_format": "roku_direct_publisher_json"
}
}
```
Highlights: deep-link aware (`Main(args)` + `roInput` runtime listener); centralized screen-stack with focus restoration; canonical Roku `Overhang` brand bar; `BusySpinner` during feed load; async feed fetch via `roUrlTransfer.asyncGetToString`.
## Error taxonomy
| Code | Meaning |
| -------------------------- | ------------------------------------------------------------------- |
| `SCHEMA_MISMATCH` | Tool input or `AppSpec` failed validation. |
| `UNKNOWN_TEMPLATE` | `spec.template` is not a registered id. |
| `UNSUPPORTED_SPEC_VERSION` | `spec.spec_version` is not supported by this server. |
| `PATH_REFUSED` | Path is on the blocklist or outside an allowed directory. |
| `OUTPUT_DIR_NOT_EMPTY` | Target directory exists and is non-empty (overwrite not requested). |
| `ASSET_NOT_FOUND` | A spec-referenced asset (icon/splash/etc.) is missing. |
| `PROJECT_DIR_INVALID` | `package_app` input is not a valid Roku project directory. |
| `RENDER_FAILED` | EJS render error inside a template. |
| `WRITE_FAILED` | Writer failed during atomic project write. |
| `ZIP_FAILED` | Packager failed. |
| `ZIP_NOT_FOUND` | `sideload_app` could not find the supplied zip. |
| `DEVICE_UNREACHABLE` | Network failure reaching the Roku. |
| `DEVICE_NOT_DEV_MODE` | Roku is not in developer mode. |
| `DEVICE_AUTH_FAILED` | HTTP Digest auth was rejected. |
| `SIDELOAD_REJECTED` | Roku returned a failure body (e.g., bad archive). |
| `SIDELOAD_TIMEOUT` | Operation timed out before the device responded. |
Every failure response carries `{ ok: false, stage, code, message, details? }`. `stage` is one of `validate`, `render`, `write`, `package`, `sideload`.
## Architecture
Strict one-way dependency flow under `src/`:
- `tools/`: MCP handlers. Composition root.
- `templates/`: Static template registry + EJS engine + render helpers. No network.
- `build/`: Atomic writer + deterministic zip packager (yazl, STORED, forced DOS timestamps). No network.
- `device/`: The ONLY module that imports a network client (`undici`). RFC 2617 Digest auth, multipart streaming via `fs.openAsBlob`, parsed Roku response markers.
- `spec/`: Shared zod schemas + error factory.
## Real-device fixtures
`test/fixtures/roku-responses/` contains the HTML response bodies the parser is tested against. They are captured against a live Roku via `scripts/smoke.ts`. Provenance and capture date live in `test/fixtures/roku-responses/README.md`.
## Determinism
No `Date.now()`, no `Math.random()`, no clock skew leak into template output or the zip. Same `AppSpec` produces the same bytes on any host.
## Contributing
Run `npm install` once after cloning. This installs the husky pre-commit hook via the `prepare` lifecycle script. Without it, your first commit bypasses the lint / format check.
The full local quality gate is:
```bash
npm run format:check && npm run lint && npm run typecheck && npm run build && npm test
```
## License
MIT; see [`LICENSE`](LICENSE).
TDQS
A4/5.0
Scored across 5 tools
Disambiguation5/5
Each tool has a clearly distinct purpose: generating a project, fetching schema, listing templates, packaging, and sideloading. No overlap exists.
Naming Consistency5/5
All tool names follow a consistent verb_noun pattern in lowercase snake_case (generate_app, get_template_schema, list_templates, package_app, sideload_app), making the set predictable.
Tool Count5/5
With 5 tools covering the core workflow of generating, packaging, and deploying Roku channels, the count is well-scoped and appropriate for the domain.
Completeness4/5
The tool surface covers the main lifecycle: template discovery, schema retrieval, project generation, packaging, and sideloading. A minor gap is the lack of a standalone validation tool, but generate_app implicitly validates the AppSpec.
Maintenance
ActivitySlowing
ResponsivenessSyncing