mcp-server-pixelcade 0.1.0__tar.gz
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.
- mcp_server_pixelcade-0.1.0/LICENSE +21 -0
- mcp_server_pixelcade-0.1.0/PKG-INFO +321 -0
- mcp_server_pixelcade-0.1.0/README.md +287 -0
- mcp_server_pixelcade-0.1.0/pyproject.toml +64 -0
- mcp_server_pixelcade-0.1.0/setup.cfg +4 -0
- mcp_server_pixelcade-0.1.0/src/mcp_server_pixelcade.egg-info/PKG-INFO +321 -0
- mcp_server_pixelcade-0.1.0/src/mcp_server_pixelcade.egg-info/SOURCES.txt +41 -0
- mcp_server_pixelcade-0.1.0/src/mcp_server_pixelcade.egg-info/dependency_links.txt +1 -0
- mcp_server_pixelcade-0.1.0/src/mcp_server_pixelcade.egg-info/entry_points.txt +2 -0
- mcp_server_pixelcade-0.1.0/src/mcp_server_pixelcade.egg-info/requires.txt +11 -0
- mcp_server_pixelcade-0.1.0/src/mcp_server_pixelcade.egg-info/top_level.txt +1 -0
- mcp_server_pixelcade-0.1.0/src/pixelcade_mcp/__init__.py +5 -0
- mcp_server_pixelcade-0.1.0/src/pixelcade_mcp/assumptions.py +230 -0
- mcp_server_pixelcade-0.1.0/src/pixelcade_mcp/cli.py +192 -0
- mcp_server_pixelcade-0.1.0/src/pixelcade_mcp/client.py +354 -0
- mcp_server_pixelcade-0.1.0/src/pixelcade_mcp/config.py +321 -0
- mcp_server_pixelcade-0.1.0/src/pixelcade_mcp/doctor.py +180 -0
- mcp_server_pixelcade-0.1.0/src/pixelcade_mcp/effects.py +178 -0
- mcp_server_pixelcade-0.1.0/src/pixelcade_mcp/limits.py +30 -0
- mcp_server_pixelcade-0.1.0/src/pixelcade_mcp/logsafe.py +309 -0
- mcp_server_pixelcade-0.1.0/src/pixelcade_mcp/lookup.py +60 -0
- mcp_server_pixelcade-0.1.0/src/pixelcade_mcp/ratelimit.py +226 -0
- mcp_server_pixelcade-0.1.0/src/pixelcade_mcp/registry.py +141 -0
- mcp_server_pixelcade-0.1.0/src/pixelcade_mcp/remote.py +516 -0
- mcp_server_pixelcade-0.1.0/src/pixelcade_mcp/server.py +950 -0
- mcp_server_pixelcade-0.1.0/src/pixelcade_mcp/sim/__init__.py +1 -0
- mcp_server_pixelcade-0.1.0/src/pixelcade_mcp/sim/fake_pixelweb.py +252 -0
- mcp_server_pixelcade-0.1.0/src/pixelcade_mcp/tasks.py +174 -0
- mcp_server_pixelcade-0.1.0/src/pixelcade_mcp/tools.py +29 -0
- mcp_server_pixelcade-0.1.0/tests/test_assumptions.py +85 -0
- mcp_server_pixelcade-0.1.0/tests/test_cli.py +191 -0
- mcp_server_pixelcade-0.1.0/tests/test_client.py +202 -0
- mcp_server_pixelcade-0.1.0/tests/test_config.py +110 -0
- mcp_server_pixelcade-0.1.0/tests/test_http_tools.py +124 -0
- mcp_server_pixelcade-0.1.0/tests/test_logsafe.py +521 -0
- mcp_server_pixelcade-0.1.0/tests/test_lookup.py +41 -0
- mcp_server_pixelcade-0.1.0/tests/test_ratelimit.py +382 -0
- mcp_server_pixelcade-0.1.0/tests/test_registry.py +175 -0
- mcp_server_pixelcade-0.1.0/tests/test_remote.py +571 -0
- mcp_server_pixelcade-0.1.0/tests/test_stdio.py +30 -0
- mcp_server_pixelcade-0.1.0/tests/test_tasks.py +425 -0
- mcp_server_pixelcade-0.1.0/tests/test_tool_annotations.py +18 -0
- mcp_server_pixelcade-0.1.0/tests/test_tools.py +592 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nicholas E. Andrade
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mcp-server-pixelcade
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MCP server for Pixelcade LED marquees and Pixelcade Pulse lighting, via the pixelweb REST API
|
|
5
|
+
Author: Nicholas E. Andrade
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/SDNick484/mcp-server-pixelcade
|
|
8
|
+
Project-URL: Issues, https://github.com/SDNick484/mcp-server-pixelcade/issues
|
|
9
|
+
Project-URL: Changelog, https://github.com/SDNick484/mcp-server-pixelcade/blob/main/CHANGELOG.md
|
|
10
|
+
Keywords: mcp,mcp-server,pixelcade,arcade,batocera,led-marquee
|
|
11
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
12
|
+
Classifier: Framework :: AsyncIO
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Classifier: Topic :: Games/Entertainment :: Arcade
|
|
20
|
+
Requires-Python: >=3.11
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: mcp<3,>=2.3
|
|
24
|
+
Requires-Dist: httpx>=0.27
|
|
25
|
+
Requires-Dist: pyjwt[crypto]>=2.10
|
|
26
|
+
Requires-Dist: uvicorn>=0.31
|
|
27
|
+
Requires-Dist: typing_extensions>=4.7
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
30
|
+
Requires-Dist: anyio>=4; extra == "dev"
|
|
31
|
+
Requires-Dist: ruff>=0.6; extra == "dev"
|
|
32
|
+
Requires-Dist: mypy>=1.11; extra == "dev"
|
|
33
|
+
Dynamic: license-file
|
|
34
|
+
|
|
35
|
+
# mcp-server-pixelcade
|
|
36
|
+
|
|
37
|
+
An [MCP](https://modelcontextprotocol.io) server for **[Pixelcade](https://pixelcade.org) LED marquees**
|
|
38
|
+
and **Pixelcade Pulse** lighting (WS2812B strips, speaker rings and matrices on a V2 board). It lets an
|
|
39
|
+
MCP client such as Claude show game and system marquees, scroll text, run arcade-themed clocks and other
|
|
40
|
+
widgets, and drive Pulse lighting effects.
|
|
41
|
+
|
|
42
|
+
> **Status: early, tested on hardware except Pulse.** On a real V2 board (pixelweb 8.0.17, Batocera 43)
|
|
43
|
+
> every marquee, text, widget and frontend tool has been checked, plus dry run; Pulse waits for strips. See
|
|
44
|
+
> [HARDWARE_VALIDATION.md](https://github.com/SDNick484/mcp-server-pixelcade/blob/main/HARDWARE_VALIDATION.md) and the [verification table](#verification-status).
|
|
45
|
+
|
|
46
|
+
This is not an official Pixelcade project.
|
|
47
|
+
|
|
48
|
+
## How it works
|
|
49
|
+
|
|
50
|
+
The Pixelcade installer runs **pixelweb**, a small HTTP listener, on the arcade host (on Batocera:
|
|
51
|
+
`/userdata/system/pixelcade/pixelweb`, port 8080, started from `/userdata/system/custom.sh`). Pixelweb
|
|
52
|
+
drives the marquee over USB and exposes a [REST API](https://pixelcade.org/developers). This server is a
|
|
53
|
+
typed, allow-listed client for that API, so it can run anywhere that can reach the arcade on TCP 8080:
|
|
54
|
+
your desktop, a laptop, or a homelab VM. Clients reach it over stdio, or over Streamable HTTP as a
|
|
55
|
+
service behind Cloudflare Access ([Service and Cloudflare](#service-and-cloudflare)).
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
MCP client ──stdio──▶ mcp-server-pixelcade ──HTTP :8080──▶ pixelweb (Batocera) ──USB──▶ V2 board ─▶ LED panels
|
|
59
|
+
└──▶ Pulse strips
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Install
|
|
63
|
+
|
|
64
|
+
```sh
|
|
65
|
+
python -m venv ~/.venvs/pixelcade && . ~/.venvs/pixelcade/bin/activate
|
|
66
|
+
pip install mcp-server-pixelcade
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
From source instead (for the newest code, or to run the tests):
|
|
70
|
+
|
|
71
|
+
```sh
|
|
72
|
+
git clone https://github.com/SDNick484/mcp-server-pixelcade.git && cd mcp-server-pixelcade
|
|
73
|
+
python -m venv .venv && . .venv/bin/activate
|
|
74
|
+
pip install -e ".[dev]" && pytest
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Requires Python 3.11+. Then check that pixelweb answers:
|
|
78
|
+
|
|
79
|
+
```sh
|
|
80
|
+
PIXELCADE_URL=192.168.1.20 mcp-server-pixelcade doctor
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`doctor` checks each layer (TCP, `/info`, the frontend's current game, Pulse) and, when one fails, says
|
|
84
|
+
what to try and which [HARDWARE_VALIDATION.md](https://github.com/SDNick484/mcp-server-pixelcade/blob/main/HARDWARE_VALIDATION.md) step covers it. It changes nothing
|
|
85
|
+
on the marquee. It ends with the [assumptions](#verification-status) that hardware contradicted (listed; the code
|
|
86
|
+
no longer relies on them) and a count of those not yet confirmed; `doctor --verbose` lists those too, and
|
|
87
|
+
`doctor --json` always includes both lists.
|
|
88
|
+
|
|
89
|
+
### Trying it without the arcade
|
|
90
|
+
|
|
91
|
+
```sh
|
|
92
|
+
mcp-server-pixelcade simulate --write-config /tmp/pxsim # a fake pixelweb; prints every request it gets
|
|
93
|
+
export PIXELCADE_CONFIG_DIR=/tmp/pxsim # in another terminal
|
|
94
|
+
mcp-server-pixelcade doctor
|
|
95
|
+
mcp-server-pixelcade call show_text text="GAME NIGHT" color=cyan # one tool call, as the model makes it
|
|
96
|
+
mcp-server-pixelcade call --dry-run blank_display # reports the GET it would send
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### On Alpine (e.g. a Proxmox LXC)
|
|
100
|
+
|
|
101
|
+
`deploy/alpine/` installs it on an always-on Alpine machine on the same LAN as the arcade, in the
|
|
102
|
+
sibling servers' `deploy/alpine/` layout so they can share an LXC: a venv in `/opt/mcp-server-pixelcade`,
|
|
103
|
+
a `mcp-pixelcade` system user, every setting in `/etc/mcp-server-pixelcade/env`, an OpenRC service
|
|
104
|
+
(`/etc/init.d/mcp-server-pixelcade`), and `mcp-server-pixelcade` on `PATH`, which runs as that user with
|
|
105
|
+
those settings, for `doctor` and `call` from a shell.
|
|
106
|
+
|
|
107
|
+
```sh
|
|
108
|
+
git clone https://github.com/SDNick484/mcp-server-pixelcade.git && cd mcp-server-pixelcade
|
|
109
|
+
sh deploy/alpine/install.sh # as root; re-run to upgrade
|
|
110
|
+
vim /etc/mcp-server-pixelcade/env # PIXELCADE_URL=<the arcade's IP>
|
|
111
|
+
mcp-server-pixelcade doctor
|
|
112
|
+
mcp-server-pixelcade call show_text text="GAME NIGHT" color=cyan
|
|
113
|
+
rc-update add mcp-server-pixelcade default && rc-service mcp-server-pixelcade start
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
A variable set in your shell beats the env file, so one-off overrides work as usual
|
|
117
|
+
(`PIXELCADE_DRY_RUN=1 mcp-server-pixelcade call ...`). mcp-server-batocera's
|
|
118
|
+
`deploy/alpine/setup-arcade.sh` installs both arcade servers in a Proxmox container in one go, and
|
|
119
|
+
enables and restarts both services. CI runs `deploy/alpine/smoke-test.sh` in a stock
|
|
120
|
+
`python:3.12-alpine` container: install, upgrade, the test suite on musl, `doctor` and `call` through the
|
|
121
|
+
command on `PATH` against the simulator, and the OpenRC service (as its user, an MCP call over HTTP,
|
|
122
|
+
Access settings from the env file).
|
|
123
|
+
|
|
124
|
+
### Service and Cloudflare
|
|
125
|
+
|
|
126
|
+
The service runs `/opt/mcp-server-pixelcade/run.sh serve --http` under `supervise-daemon` (restarted if
|
|
127
|
+
it exits) as `mcp-pixelcade`, logging to `/var/log/mcp-server-pixelcade/server.log`. The HTTP transport
|
|
128
|
+
is `remote.py`, shared byte for byte with the sibling servers through
|
|
129
|
+
[mcp-server-shared](https://github.com/SDNick484/mcp-server-shared). Its settings are the `MCP_HTTP_*`,
|
|
130
|
+
`MCP_PUBLIC_HOSTS` and `CF_ACCESS_*` lines in `/etc/mcp-server-pixelcade/env`:
|
|
131
|
+
|
|
132
|
+
| Flag | Variable | Default |
|
|
133
|
+
| --- | --- | --- |
|
|
134
|
+
| `--bind` | `MCP_HTTP_BIND` | `127.0.0.1` |
|
|
135
|
+
| `--port` | `MCP_HTTP_PORT` | `8718` (the siblings use 8711 to 8716, Batocera 8717) |
|
|
136
|
+
| `--path` | `MCP_HTTP_PATH` | `/pixelcade/mcp` |
|
|
137
|
+
| `--public-host` | `MCP_PUBLIC_HOSTS` | none: only loopback Host headers pass |
|
|
138
|
+
| | `CF_ACCESS_TEAM_DOMAIN`, `CF_ACCESS_AUD` | none: serves only this machine |
|
|
139
|
+
| | `MCP_ALLOWED_EMAILS` | anyone Access lets in |
|
|
140
|
+
|
|
141
|
+
With Access configured, every request needs a valid Access assertion, checked here: signature (Access's
|
|
142
|
+
published keys), the application's audience tag, issuer, expiry, and the email allow-list if set; anything
|
|
143
|
+
else gets a plain 403. Without Access it serves only the machine it runs on: it won't listen on anything
|
|
144
|
+
but loopback, won't start with a `--public-host`, and answers 403 to anything a tunnel or proxy forwarded
|
|
145
|
+
(`--insecure-no-auth` turns that off for a short trusted-LAN test; don't run the service with it).
|
|
146
|
+
|
|
147
|
+
To use it as a connector, with cloudflared running on the same machine (keep the `127.0.0.1` bind):
|
|
148
|
+
|
|
149
|
+
1. **Tunnel route:** a public hostname, say `mcp.example.com`, path `pixelcade/*`, to
|
|
150
|
+
`http://localhost:8718`. Several servers can share the hostname, one path each.
|
|
151
|
+
2. **Access application** (self-hosted) covering that hostname and path, with a policy for who may use it
|
|
152
|
+
and Managed OAuth turned on, so Claude's connector can sign in through Access. Note its AUD tag.
|
|
153
|
+
3. **The env file:** `MCP_PUBLIC_HOSTS=mcp.example.com`, `CF_ACCESS_TEAM_DOMAIN=<team>.cloudflareaccess.com`,
|
|
154
|
+
`CF_ACCESS_AUD=<the AUD tag>`, then `rc-service mcp-server-pixelcade restart`.
|
|
155
|
+
4. **The connector:** `https://mcp.example.com/pixelcade/mcp`.
|
|
156
|
+
|
|
157
|
+
`GET /healthz` answers without auth, for monitoring. Details are in `src/pixelcade_mcp/remote.py`.
|
|
158
|
+
|
|
159
|
+
## Use it with an MCP client
|
|
160
|
+
|
|
161
|
+
```json
|
|
162
|
+
{
|
|
163
|
+
"mcpServers": {
|
|
164
|
+
"pixelcade": {
|
|
165
|
+
"command": "/path/to/mcp-server-pixelcade/.venv/bin/mcp-server-pixelcade",
|
|
166
|
+
"env": { "PIXELCADE_URL": "192.168.1.50" }
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
For Claude Code: `claude mcp add pixelcade -e PIXELCADE_URL=192.168.1.50 -- /path/to/.venv/bin/mcp-server-pixelcade`.
|
|
173
|
+
|
|
174
|
+
Running as a service behind Cloudflare Access instead, add `https://<your host>/pixelcade/mcp` as a
|
|
175
|
+
connector ([Service and Cloudflare](#service-and-cloudflare)).
|
|
176
|
+
|
|
177
|
+
## Configuration
|
|
178
|
+
|
|
179
|
+
| Setting | Default | What it does |
|
|
180
|
+
| -------------------------------- | ------------------------------------- | -------------------------------------------------------------- |
|
|
181
|
+
| `PIXELCADE_URL` | `http://batocera.local:8080` | Where pixelweb listens. A bare host or IP gets `http://` and `:8080` |
|
|
182
|
+
| `PIXELCADE_PULSE_MAX_BRIGHTNESS` | `128` | Highest Pulse brightness/intensity the model may set (0-255) |
|
|
183
|
+
| `PIXELCADE_TIMEOUT` | `10` | Seconds to wait for pixelweb (1-60) |
|
|
184
|
+
| `PIXELCADE_CONFIG_DIR` | `$XDG_CONFIG_HOME/mcp-server-pixelcade` | Where an optional `config.json` lives |
|
|
185
|
+
| `PIXELCADE_NAME` | pixelweb's URL | How results name the marquee |
|
|
186
|
+
| `PIXELCADE_DRY_RUN` | off | Read pixelweb, change nothing (`serve --dry-run`) |
|
|
187
|
+
| `PIXELCADE_LOG_UNREDACTED` | off | Show LAN addresses in logs (`--no-redact`); secrets stay hidden |
|
|
188
|
+
| `PIXELCADE_DEBUG` | off | Debug logging (`--debug`) |
|
|
189
|
+
|
|
190
|
+
`config.json` can hold the same settings as `url`, `pulse_max_brightness`, `timeout_s`, `name` and `dry_run`; environment
|
|
191
|
+
variables win. `.local` names need mDNS, which usually doesn't cross VLANs or work in WSL2 without
|
|
192
|
+
mirrored networking. A DHCP reservation plus the IP address is the dependable choice.
|
|
193
|
+
|
|
194
|
+
## Tools
|
|
195
|
+
|
|
196
|
+
All tools use the `pixelcade_` prefix to eliminate namespace collisions with sibling MCP servers in multi-server agent environments. The CLI supports shorthand names (`call show_text` resolves to `pixelcade_show_text`).
|
|
197
|
+
|
|
198
|
+
| Tool | What it does |
|
|
199
|
+
| -------------------------------- | ---------------------------------------------------------------------------------------------- |
|
|
200
|
+
| `pixelcade_get_status` | Reachability, hardware/firmware/software versions, the frontend's selected game, Pulse state |
|
|
201
|
+
| `pixelcade_list_artwork` | The marquees in one folder (a system, `console`, `animations`, `user`): names, still/animated |
|
|
202
|
+
| `pixelcade_list_pulse_effects` | Pulse effect names by category, per-game/per-system name patterns, and zone names |
|
|
203
|
+
| `pixelcade_show_game_marquee` | A game's marquee by system + ROM name; still/animated choice, optional queueing and fallback text |
|
|
204
|
+
| `pixelcade_show_console_marquee` | A system marquee, e.g. `snes` |
|
|
205
|
+
| `pixelcade_show_current_game` | Put back the marquee for the game selected in EmulationStation |
|
|
206
|
+
| `pixelcade_show_default_marquee` | Pixelcade's default marquee (needs pixelweb 8.0.14+) |
|
|
207
|
+
| `pixelcade_show_text` | Scrolling or fixed text: color, speed, loops, 1/2/4 lines, blink, left-justify |
|
|
208
|
+
| `pixelcade_play_animation` | One of Pixelcade's built-in animations |
|
|
209
|
+
| `pixelcade_show_widget` | Clock (19 arcade themes), weather, stocks, sports scores, or the attract-mode playlist |
|
|
210
|
+
| `pixelcade_stop_widgets` | Stop widgets/attract mode and return to arcade mode |
|
|
211
|
+
| `pixelcade_blank_display` | Black screen |
|
|
212
|
+
| `pixelcade_pulse_effect` | Run an effect on a Pulse zone (`rainbow`, `fire`, `mame_pacman`, …) with colors, speed, loops |
|
|
213
|
+
| `pixelcade_pulse_fill` | Solid color on a zone |
|
|
214
|
+
| `pixelcade_pulse_clear` | Turn off one zone, or all LEDs |
|
|
215
|
+
| `pixelcade_set_pulse_brightness` | Overall Pulse brightness, up to the server's cap |
|
|
216
|
+
|
|
217
|
+
**The frontend drives the marquee too.** EmulationStation's Pixelcade hooks call pixelweb whenever you
|
|
218
|
+
scroll to a system or game, and every game launch calls `/attract/stop`. So whatever the model shows
|
|
219
|
+
lasts until the next frontend event. `show_current_game` restores the frontend's marquee, and
|
|
220
|
+
`show_default_marquee` shows the default one.
|
|
221
|
+
|
|
222
|
+
**Locks.** `show_widget(lock=true)` sends `?nointerrupt`, which Pixelcade documents as preventing
|
|
223
|
+
interruption by *other API calls*, this server's included, until `stop_widgets` or a game launch.
|
|
224
|
+
Pixelcade's Batocera screensaver hooks do the same: the screensaver starts attract mode with
|
|
225
|
+
`?nointerrupt` and ends it with `/attract/stop`. So while the screensaver runs, marquee calls succeed
|
|
226
|
+
but change nothing (PX-ATTRACT-LOCK); `stop_widgets` releases the lock.
|
|
227
|
+
|
|
228
|
+
**Pixelcade 8.x.** Pixelweb 8 is mostly about Sidekick companion displays; the API this server uses is
|
|
229
|
+
unchanged. It adds `/defaultmarquee` (8.0.14, PX-DEFAULT-MARQUEE), and a companion-app setting to show
|
|
230
|
+
static PNG marquees only, which may override `show_game_marquee(animation="animated")`. `doctor` shows
|
|
231
|
+
the pixelweb version and notes when it's too old for `show_default_marquee`.
|
|
232
|
+
|
|
233
|
+
**Names:** `system` is the Batocera ROM folder name (`mame`, `fbneo`, `snes`, …) and `rom` is the file
|
|
234
|
+
name without its extension (`pacman`, `Super Mario World (USA)`), the same values Batocera's own hook
|
|
235
|
+
scripts send.
|
|
236
|
+
|
|
237
|
+
Every tool that changes the display returns the same structured result as the sibling servers:
|
|
238
|
+
|
|
239
|
+
```json
|
|
240
|
+
{ "pixelcade": "Arcade marquee", "outcome": "done", "detail": "Showing the text",
|
|
241
|
+
"sent": ["GET /text?t=GAME%20NIGHT&c=cyan"], "warnings": [] }
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
`outcome` is `done` or `dry_run`; `sent` is the exact GET (or, in a dry run, the one it would send).
|
|
245
|
+
|
|
246
|
+
## Safety design
|
|
247
|
+
|
|
248
|
+
- **Namespace isolation.** Every tool is prefixed with `pixelcade_` to eliminate collision with other MCP servers in multi-server configurations.
|
|
249
|
+
- **Hardware Rate Limiting.** Commands are regulated via TokenBucket rate limiters:
|
|
250
|
+
- USB serial queue protection (burst 6, refill 2/s) prevents buffer overruns on the V2 board.
|
|
251
|
+
- High-draw Pulse LED protection (burst 3, refill 0.5/s) prevents thermal and current overload.
|
|
252
|
+
- **Network topology & PII masking.** Network URLs and IP addresses are masked (e.g. `http://x.x.x.50:8080`) in tool outputs, logs, and error messages via `mcp_shared.logsafe`.
|
|
253
|
+
- **A fixed set of endpoints.** Paths are built in `server.py` from constants plus validated arguments.
|
|
254
|
+
Pixelweb also serves `/quit`, `/reboot`, `/shutdown`, `/arcade/write` (writes the marquee's SD card),
|
|
255
|
+
`/upload`, `/settings`, `/update/*` and `/ledstrip/numpixels`; none are reachable through this server.
|
|
256
|
+
- **Arguments can't become other URLs.** Each model-supplied path value must be a single segment (no
|
|
257
|
+
`/`, `\`, `.`/`..` or control characters) and is then percent-encoded, `/` included. Zones, colors,
|
|
258
|
+
tickers and team codes are validated; effect names must match a strict pattern.
|
|
259
|
+
- **A Pulse brightness cap**, 128 by default. Each WS2812B LED can draw 60 mA at full white, and Pixelcade
|
|
260
|
+
warns that powering more than about 40 LEDs from the V2 board without an external 5V supply (with a
|
|
261
|
+
common ground) can overload it. Raise `PIXELCADE_PULSE_MAX_BRIGHTNESS` once your strips have their own
|
|
262
|
+
supply. The firmware's own thermal throttling (55/60/65 °C) is a backstop, not a plan.
|
|
263
|
+
- **Pixelweb has no authentication**, so anything on your LAN can already call every endpoint. Keep port
|
|
264
|
+
8080 off the internet. Over HTTP, this server is the gate: Access assertions are checked on every
|
|
265
|
+
request, and without Access it serves only its own machine ([Service and Cloudflare](#service-and-cloudflare)).
|
|
266
|
+
- Tools carry titles and MCP annotations (`readOnlyHint`, `destructiveHint`, `idempotentHint`,
|
|
267
|
+
`openWorldHint`). Nothing is marked destructive: every display change is undone by the next one.
|
|
268
|
+
- Structured XML docstrings (`<use_case>` and `<important_notes>`) guide models on tool preconditions, display lifetimes, and routing.
|
|
269
|
+
- Errors are actionable ("pixelweb isn't answering at …", "Can't resolve batocera.local; set
|
|
270
|
+
PIXELCADE_URL to the IP"), and logs go to stderr because stdout belongs to the MCP transport.
|
|
271
|
+
|
|
272
|
+
## Verification status
|
|
273
|
+
|
|
274
|
+
Every pixelweb behavior the code relies on is listed in `src/pixelcade_mcp/assumptions.py`, with its
|
|
275
|
+
source and how sure we are; the code cites each id where it depends on it, and
|
|
276
|
+
[HARDWARE_VALIDATION.md](https://github.com/SDNick484/mcp-server-pixelcade/blob/main/HARDWARE_VALIDATION.md) has the step that confirms it. *high* means documented
|
|
277
|
+
and used the same way by Pixelcade's own scripts; *medium* means documented only; *low* means our own
|
|
278
|
+
guess. Hardware results so far are from pixelweb 8.0.17 on a V2 board (2026-10-22).
|
|
279
|
+
|
|
280
|
+
| Assumption | Confidence | Status |
|
|
281
|
+
|---|---|---|
|
|
282
|
+
| `PX-GET-API` | high | hardware-verified |
|
|
283
|
+
| `PX-INFO` | medium | hardware-verified |
|
|
284
|
+
| `PX-CURRENTGAME` | medium | hardware-verified |
|
|
285
|
+
| `PX-LISTS` | low | hardware-contradicted |
|
|
286
|
+
| `PX-IMAGES-LIST` | high | hardware-verified |
|
|
287
|
+
| `PX-REPLIES` | low | hardware-verified |
|
|
288
|
+
| `PX-TEXT` | medium | hardware-verified |
|
|
289
|
+
| `PX-QUEUE` | medium | hardware-verified |
|
|
290
|
+
| `PX-HOLD` | medium | hardware-verified |
|
|
291
|
+
| `PX-FRONTEND` | high | hardware-verified |
|
|
292
|
+
| `PX-WIDGETS` | medium | hardware-verified |
|
|
293
|
+
| `PX-ATTRACT-LOCK` | medium | hardware-verified |
|
|
294
|
+
| `PX-DEFAULT-MARQUEE` | low | hardware-verified |
|
|
295
|
+
| `PX-ZONES` | medium | simulator-only |
|
|
296
|
+
| `PX-BRIGHTNESS` | medium | simulator-only |
|
|
297
|
+
| `PX-POWER` | high | simulator-only |
|
|
298
|
+
|
|
299
|
+
## Not yet included
|
|
300
|
+
|
|
301
|
+
- Pulse **batch zone control**, **MAX7219** 7-segment/dot-matrix displays, and **mini LCD** (`/ministats`)
|
|
302
|
+
endpoints, which Pixelcade documents but this server doesn't wrap yet.
|
|
303
|
+
- Editing the attract-mode playlist, and OAuth-backed widgets (Spotify, Plex, YouTube, Google Calendar).
|
|
304
|
+
- Uploading custom artwork (`/upload`): useful, but it writes files on the arcade, so it should be opt-in.
|
|
305
|
+
|
|
306
|
+
## Development
|
|
307
|
+
|
|
308
|
+
```sh
|
|
309
|
+
pip install -e ".[dev]"
|
|
310
|
+
pytest # validators, HTTP client, tools via an in-process MCP client, dry run, faults, CLI, stdio, HTTP + Access
|
|
311
|
+
ruff check . && ruff format --check .
|
|
312
|
+
mypy # strict
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
Tests run against the wire-level fake pixelweb (`sim/fake_pixelweb.py`), which records each request's raw
|
|
316
|
+
path, so the exact percent-encoding on the wire is asserted. To poke at the tools interactively:
|
|
317
|
+
`npx @modelcontextprotocol/inspector mcp-server-pixelcade`.
|
|
318
|
+
|
|
319
|
+
## License
|
|
320
|
+
|
|
321
|
+
MIT. See [LICENSE](https://github.com/SDNick484/mcp-server-pixelcade/blob/main/LICENSE).
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
# mcp-server-pixelcade
|
|
2
|
+
|
|
3
|
+
An [MCP](https://modelcontextprotocol.io) server for **[Pixelcade](https://pixelcade.org) LED marquees**
|
|
4
|
+
and **Pixelcade Pulse** lighting (WS2812B strips, speaker rings and matrices on a V2 board). It lets an
|
|
5
|
+
MCP client such as Claude show game and system marquees, scroll text, run arcade-themed clocks and other
|
|
6
|
+
widgets, and drive Pulse lighting effects.
|
|
7
|
+
|
|
8
|
+
> **Status: early, tested on hardware except Pulse.** On a real V2 board (pixelweb 8.0.17, Batocera 43)
|
|
9
|
+
> every marquee, text, widget and frontend tool has been checked, plus dry run; Pulse waits for strips. See
|
|
10
|
+
> [HARDWARE_VALIDATION.md](https://github.com/SDNick484/mcp-server-pixelcade/blob/main/HARDWARE_VALIDATION.md) and the [verification table](#verification-status).
|
|
11
|
+
|
|
12
|
+
This is not an official Pixelcade project.
|
|
13
|
+
|
|
14
|
+
## How it works
|
|
15
|
+
|
|
16
|
+
The Pixelcade installer runs **pixelweb**, a small HTTP listener, on the arcade host (on Batocera:
|
|
17
|
+
`/userdata/system/pixelcade/pixelweb`, port 8080, started from `/userdata/system/custom.sh`). Pixelweb
|
|
18
|
+
drives the marquee over USB and exposes a [REST API](https://pixelcade.org/developers). This server is a
|
|
19
|
+
typed, allow-listed client for that API, so it can run anywhere that can reach the arcade on TCP 8080:
|
|
20
|
+
your desktop, a laptop, or a homelab VM. Clients reach it over stdio, or over Streamable HTTP as a
|
|
21
|
+
service behind Cloudflare Access ([Service and Cloudflare](#service-and-cloudflare)).
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
MCP client ──stdio──▶ mcp-server-pixelcade ──HTTP :8080──▶ pixelweb (Batocera) ──USB──▶ V2 board ─▶ LED panels
|
|
25
|
+
└──▶ Pulse strips
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Install
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
python -m venv ~/.venvs/pixelcade && . ~/.venvs/pixelcade/bin/activate
|
|
32
|
+
pip install mcp-server-pixelcade
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
From source instead (for the newest code, or to run the tests):
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
git clone https://github.com/SDNick484/mcp-server-pixelcade.git && cd mcp-server-pixelcade
|
|
39
|
+
python -m venv .venv && . .venv/bin/activate
|
|
40
|
+
pip install -e ".[dev]" && pytest
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Requires Python 3.11+. Then check that pixelweb answers:
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
PIXELCADE_URL=192.168.1.20 mcp-server-pixelcade doctor
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
`doctor` checks each layer (TCP, `/info`, the frontend's current game, Pulse) and, when one fails, says
|
|
50
|
+
what to try and which [HARDWARE_VALIDATION.md](https://github.com/SDNick484/mcp-server-pixelcade/blob/main/HARDWARE_VALIDATION.md) step covers it. It changes nothing
|
|
51
|
+
on the marquee. It ends with the [assumptions](#verification-status) that hardware contradicted (listed; the code
|
|
52
|
+
no longer relies on them) and a count of those not yet confirmed; `doctor --verbose` lists those too, and
|
|
53
|
+
`doctor --json` always includes both lists.
|
|
54
|
+
|
|
55
|
+
### Trying it without the arcade
|
|
56
|
+
|
|
57
|
+
```sh
|
|
58
|
+
mcp-server-pixelcade simulate --write-config /tmp/pxsim # a fake pixelweb; prints every request it gets
|
|
59
|
+
export PIXELCADE_CONFIG_DIR=/tmp/pxsim # in another terminal
|
|
60
|
+
mcp-server-pixelcade doctor
|
|
61
|
+
mcp-server-pixelcade call show_text text="GAME NIGHT" color=cyan # one tool call, as the model makes it
|
|
62
|
+
mcp-server-pixelcade call --dry-run blank_display # reports the GET it would send
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### On Alpine (e.g. a Proxmox LXC)
|
|
66
|
+
|
|
67
|
+
`deploy/alpine/` installs it on an always-on Alpine machine on the same LAN as the arcade, in the
|
|
68
|
+
sibling servers' `deploy/alpine/` layout so they can share an LXC: a venv in `/opt/mcp-server-pixelcade`,
|
|
69
|
+
a `mcp-pixelcade` system user, every setting in `/etc/mcp-server-pixelcade/env`, an OpenRC service
|
|
70
|
+
(`/etc/init.d/mcp-server-pixelcade`), and `mcp-server-pixelcade` on `PATH`, which runs as that user with
|
|
71
|
+
those settings, for `doctor` and `call` from a shell.
|
|
72
|
+
|
|
73
|
+
```sh
|
|
74
|
+
git clone https://github.com/SDNick484/mcp-server-pixelcade.git && cd mcp-server-pixelcade
|
|
75
|
+
sh deploy/alpine/install.sh # as root; re-run to upgrade
|
|
76
|
+
vim /etc/mcp-server-pixelcade/env # PIXELCADE_URL=<the arcade's IP>
|
|
77
|
+
mcp-server-pixelcade doctor
|
|
78
|
+
mcp-server-pixelcade call show_text text="GAME NIGHT" color=cyan
|
|
79
|
+
rc-update add mcp-server-pixelcade default && rc-service mcp-server-pixelcade start
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
A variable set in your shell beats the env file, so one-off overrides work as usual
|
|
83
|
+
(`PIXELCADE_DRY_RUN=1 mcp-server-pixelcade call ...`). mcp-server-batocera's
|
|
84
|
+
`deploy/alpine/setup-arcade.sh` installs both arcade servers in a Proxmox container in one go, and
|
|
85
|
+
enables and restarts both services. CI runs `deploy/alpine/smoke-test.sh` in a stock
|
|
86
|
+
`python:3.12-alpine` container: install, upgrade, the test suite on musl, `doctor` and `call` through the
|
|
87
|
+
command on `PATH` against the simulator, and the OpenRC service (as its user, an MCP call over HTTP,
|
|
88
|
+
Access settings from the env file).
|
|
89
|
+
|
|
90
|
+
### Service and Cloudflare
|
|
91
|
+
|
|
92
|
+
The service runs `/opt/mcp-server-pixelcade/run.sh serve --http` under `supervise-daemon` (restarted if
|
|
93
|
+
it exits) as `mcp-pixelcade`, logging to `/var/log/mcp-server-pixelcade/server.log`. The HTTP transport
|
|
94
|
+
is `remote.py`, shared byte for byte with the sibling servers through
|
|
95
|
+
[mcp-server-shared](https://github.com/SDNick484/mcp-server-shared). Its settings are the `MCP_HTTP_*`,
|
|
96
|
+
`MCP_PUBLIC_HOSTS` and `CF_ACCESS_*` lines in `/etc/mcp-server-pixelcade/env`:
|
|
97
|
+
|
|
98
|
+
| Flag | Variable | Default |
|
|
99
|
+
| --- | --- | --- |
|
|
100
|
+
| `--bind` | `MCP_HTTP_BIND` | `127.0.0.1` |
|
|
101
|
+
| `--port` | `MCP_HTTP_PORT` | `8718` (the siblings use 8711 to 8716, Batocera 8717) |
|
|
102
|
+
| `--path` | `MCP_HTTP_PATH` | `/pixelcade/mcp` |
|
|
103
|
+
| `--public-host` | `MCP_PUBLIC_HOSTS` | none: only loopback Host headers pass |
|
|
104
|
+
| | `CF_ACCESS_TEAM_DOMAIN`, `CF_ACCESS_AUD` | none: serves only this machine |
|
|
105
|
+
| | `MCP_ALLOWED_EMAILS` | anyone Access lets in |
|
|
106
|
+
|
|
107
|
+
With Access configured, every request needs a valid Access assertion, checked here: signature (Access's
|
|
108
|
+
published keys), the application's audience tag, issuer, expiry, and the email allow-list if set; anything
|
|
109
|
+
else gets a plain 403. Without Access it serves only the machine it runs on: it won't listen on anything
|
|
110
|
+
but loopback, won't start with a `--public-host`, and answers 403 to anything a tunnel or proxy forwarded
|
|
111
|
+
(`--insecure-no-auth` turns that off for a short trusted-LAN test; don't run the service with it).
|
|
112
|
+
|
|
113
|
+
To use it as a connector, with cloudflared running on the same machine (keep the `127.0.0.1` bind):
|
|
114
|
+
|
|
115
|
+
1. **Tunnel route:** a public hostname, say `mcp.example.com`, path `pixelcade/*`, to
|
|
116
|
+
`http://localhost:8718`. Several servers can share the hostname, one path each.
|
|
117
|
+
2. **Access application** (self-hosted) covering that hostname and path, with a policy for who may use it
|
|
118
|
+
and Managed OAuth turned on, so Claude's connector can sign in through Access. Note its AUD tag.
|
|
119
|
+
3. **The env file:** `MCP_PUBLIC_HOSTS=mcp.example.com`, `CF_ACCESS_TEAM_DOMAIN=<team>.cloudflareaccess.com`,
|
|
120
|
+
`CF_ACCESS_AUD=<the AUD tag>`, then `rc-service mcp-server-pixelcade restart`.
|
|
121
|
+
4. **The connector:** `https://mcp.example.com/pixelcade/mcp`.
|
|
122
|
+
|
|
123
|
+
`GET /healthz` answers without auth, for monitoring. Details are in `src/pixelcade_mcp/remote.py`.
|
|
124
|
+
|
|
125
|
+
## Use it with an MCP client
|
|
126
|
+
|
|
127
|
+
```json
|
|
128
|
+
{
|
|
129
|
+
"mcpServers": {
|
|
130
|
+
"pixelcade": {
|
|
131
|
+
"command": "/path/to/mcp-server-pixelcade/.venv/bin/mcp-server-pixelcade",
|
|
132
|
+
"env": { "PIXELCADE_URL": "192.168.1.50" }
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
For Claude Code: `claude mcp add pixelcade -e PIXELCADE_URL=192.168.1.50 -- /path/to/.venv/bin/mcp-server-pixelcade`.
|
|
139
|
+
|
|
140
|
+
Running as a service behind Cloudflare Access instead, add `https://<your host>/pixelcade/mcp` as a
|
|
141
|
+
connector ([Service and Cloudflare](#service-and-cloudflare)).
|
|
142
|
+
|
|
143
|
+
## Configuration
|
|
144
|
+
|
|
145
|
+
| Setting | Default | What it does |
|
|
146
|
+
| -------------------------------- | ------------------------------------- | -------------------------------------------------------------- |
|
|
147
|
+
| `PIXELCADE_URL` | `http://batocera.local:8080` | Where pixelweb listens. A bare host or IP gets `http://` and `:8080` |
|
|
148
|
+
| `PIXELCADE_PULSE_MAX_BRIGHTNESS` | `128` | Highest Pulse brightness/intensity the model may set (0-255) |
|
|
149
|
+
| `PIXELCADE_TIMEOUT` | `10` | Seconds to wait for pixelweb (1-60) |
|
|
150
|
+
| `PIXELCADE_CONFIG_DIR` | `$XDG_CONFIG_HOME/mcp-server-pixelcade` | Where an optional `config.json` lives |
|
|
151
|
+
| `PIXELCADE_NAME` | pixelweb's URL | How results name the marquee |
|
|
152
|
+
| `PIXELCADE_DRY_RUN` | off | Read pixelweb, change nothing (`serve --dry-run`) |
|
|
153
|
+
| `PIXELCADE_LOG_UNREDACTED` | off | Show LAN addresses in logs (`--no-redact`); secrets stay hidden |
|
|
154
|
+
| `PIXELCADE_DEBUG` | off | Debug logging (`--debug`) |
|
|
155
|
+
|
|
156
|
+
`config.json` can hold the same settings as `url`, `pulse_max_brightness`, `timeout_s`, `name` and `dry_run`; environment
|
|
157
|
+
variables win. `.local` names need mDNS, which usually doesn't cross VLANs or work in WSL2 without
|
|
158
|
+
mirrored networking. A DHCP reservation plus the IP address is the dependable choice.
|
|
159
|
+
|
|
160
|
+
## Tools
|
|
161
|
+
|
|
162
|
+
All tools use the `pixelcade_` prefix to eliminate namespace collisions with sibling MCP servers in multi-server agent environments. The CLI supports shorthand names (`call show_text` resolves to `pixelcade_show_text`).
|
|
163
|
+
|
|
164
|
+
| Tool | What it does |
|
|
165
|
+
| -------------------------------- | ---------------------------------------------------------------------------------------------- |
|
|
166
|
+
| `pixelcade_get_status` | Reachability, hardware/firmware/software versions, the frontend's selected game, Pulse state |
|
|
167
|
+
| `pixelcade_list_artwork` | The marquees in one folder (a system, `console`, `animations`, `user`): names, still/animated |
|
|
168
|
+
| `pixelcade_list_pulse_effects` | Pulse effect names by category, per-game/per-system name patterns, and zone names |
|
|
169
|
+
| `pixelcade_show_game_marquee` | A game's marquee by system + ROM name; still/animated choice, optional queueing and fallback text |
|
|
170
|
+
| `pixelcade_show_console_marquee` | A system marquee, e.g. `snes` |
|
|
171
|
+
| `pixelcade_show_current_game` | Put back the marquee for the game selected in EmulationStation |
|
|
172
|
+
| `pixelcade_show_default_marquee` | Pixelcade's default marquee (needs pixelweb 8.0.14+) |
|
|
173
|
+
| `pixelcade_show_text` | Scrolling or fixed text: color, speed, loops, 1/2/4 lines, blink, left-justify |
|
|
174
|
+
| `pixelcade_play_animation` | One of Pixelcade's built-in animations |
|
|
175
|
+
| `pixelcade_show_widget` | Clock (19 arcade themes), weather, stocks, sports scores, or the attract-mode playlist |
|
|
176
|
+
| `pixelcade_stop_widgets` | Stop widgets/attract mode and return to arcade mode |
|
|
177
|
+
| `pixelcade_blank_display` | Black screen |
|
|
178
|
+
| `pixelcade_pulse_effect` | Run an effect on a Pulse zone (`rainbow`, `fire`, `mame_pacman`, …) with colors, speed, loops |
|
|
179
|
+
| `pixelcade_pulse_fill` | Solid color on a zone |
|
|
180
|
+
| `pixelcade_pulse_clear` | Turn off one zone, or all LEDs |
|
|
181
|
+
| `pixelcade_set_pulse_brightness` | Overall Pulse brightness, up to the server's cap |
|
|
182
|
+
|
|
183
|
+
**The frontend drives the marquee too.** EmulationStation's Pixelcade hooks call pixelweb whenever you
|
|
184
|
+
scroll to a system or game, and every game launch calls `/attract/stop`. So whatever the model shows
|
|
185
|
+
lasts until the next frontend event. `show_current_game` restores the frontend's marquee, and
|
|
186
|
+
`show_default_marquee` shows the default one.
|
|
187
|
+
|
|
188
|
+
**Locks.** `show_widget(lock=true)` sends `?nointerrupt`, which Pixelcade documents as preventing
|
|
189
|
+
interruption by *other API calls*, this server's included, until `stop_widgets` or a game launch.
|
|
190
|
+
Pixelcade's Batocera screensaver hooks do the same: the screensaver starts attract mode with
|
|
191
|
+
`?nointerrupt` and ends it with `/attract/stop`. So while the screensaver runs, marquee calls succeed
|
|
192
|
+
but change nothing (PX-ATTRACT-LOCK); `stop_widgets` releases the lock.
|
|
193
|
+
|
|
194
|
+
**Pixelcade 8.x.** Pixelweb 8 is mostly about Sidekick companion displays; the API this server uses is
|
|
195
|
+
unchanged. It adds `/defaultmarquee` (8.0.14, PX-DEFAULT-MARQUEE), and a companion-app setting to show
|
|
196
|
+
static PNG marquees only, which may override `show_game_marquee(animation="animated")`. `doctor` shows
|
|
197
|
+
the pixelweb version and notes when it's too old for `show_default_marquee`.
|
|
198
|
+
|
|
199
|
+
**Names:** `system` is the Batocera ROM folder name (`mame`, `fbneo`, `snes`, …) and `rom` is the file
|
|
200
|
+
name without its extension (`pacman`, `Super Mario World (USA)`), the same values Batocera's own hook
|
|
201
|
+
scripts send.
|
|
202
|
+
|
|
203
|
+
Every tool that changes the display returns the same structured result as the sibling servers:
|
|
204
|
+
|
|
205
|
+
```json
|
|
206
|
+
{ "pixelcade": "Arcade marquee", "outcome": "done", "detail": "Showing the text",
|
|
207
|
+
"sent": ["GET /text?t=GAME%20NIGHT&c=cyan"], "warnings": [] }
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
`outcome` is `done` or `dry_run`; `sent` is the exact GET (or, in a dry run, the one it would send).
|
|
211
|
+
|
|
212
|
+
## Safety design
|
|
213
|
+
|
|
214
|
+
- **Namespace isolation.** Every tool is prefixed with `pixelcade_` to eliminate collision with other MCP servers in multi-server configurations.
|
|
215
|
+
- **Hardware Rate Limiting.** Commands are regulated via TokenBucket rate limiters:
|
|
216
|
+
- USB serial queue protection (burst 6, refill 2/s) prevents buffer overruns on the V2 board.
|
|
217
|
+
- High-draw Pulse LED protection (burst 3, refill 0.5/s) prevents thermal and current overload.
|
|
218
|
+
- **Network topology & PII masking.** Network URLs and IP addresses are masked (e.g. `http://x.x.x.50:8080`) in tool outputs, logs, and error messages via `mcp_shared.logsafe`.
|
|
219
|
+
- **A fixed set of endpoints.** Paths are built in `server.py` from constants plus validated arguments.
|
|
220
|
+
Pixelweb also serves `/quit`, `/reboot`, `/shutdown`, `/arcade/write` (writes the marquee's SD card),
|
|
221
|
+
`/upload`, `/settings`, `/update/*` and `/ledstrip/numpixels`; none are reachable through this server.
|
|
222
|
+
- **Arguments can't become other URLs.** Each model-supplied path value must be a single segment (no
|
|
223
|
+
`/`, `\`, `.`/`..` or control characters) and is then percent-encoded, `/` included. Zones, colors,
|
|
224
|
+
tickers and team codes are validated; effect names must match a strict pattern.
|
|
225
|
+
- **A Pulse brightness cap**, 128 by default. Each WS2812B LED can draw 60 mA at full white, and Pixelcade
|
|
226
|
+
warns that powering more than about 40 LEDs from the V2 board without an external 5V supply (with a
|
|
227
|
+
common ground) can overload it. Raise `PIXELCADE_PULSE_MAX_BRIGHTNESS` once your strips have their own
|
|
228
|
+
supply. The firmware's own thermal throttling (55/60/65 °C) is a backstop, not a plan.
|
|
229
|
+
- **Pixelweb has no authentication**, so anything on your LAN can already call every endpoint. Keep port
|
|
230
|
+
8080 off the internet. Over HTTP, this server is the gate: Access assertions are checked on every
|
|
231
|
+
request, and without Access it serves only its own machine ([Service and Cloudflare](#service-and-cloudflare)).
|
|
232
|
+
- Tools carry titles and MCP annotations (`readOnlyHint`, `destructiveHint`, `idempotentHint`,
|
|
233
|
+
`openWorldHint`). Nothing is marked destructive: every display change is undone by the next one.
|
|
234
|
+
- Structured XML docstrings (`<use_case>` and `<important_notes>`) guide models on tool preconditions, display lifetimes, and routing.
|
|
235
|
+
- Errors are actionable ("pixelweb isn't answering at …", "Can't resolve batocera.local; set
|
|
236
|
+
PIXELCADE_URL to the IP"), and logs go to stderr because stdout belongs to the MCP transport.
|
|
237
|
+
|
|
238
|
+
## Verification status
|
|
239
|
+
|
|
240
|
+
Every pixelweb behavior the code relies on is listed in `src/pixelcade_mcp/assumptions.py`, with its
|
|
241
|
+
source and how sure we are; the code cites each id where it depends on it, and
|
|
242
|
+
[HARDWARE_VALIDATION.md](https://github.com/SDNick484/mcp-server-pixelcade/blob/main/HARDWARE_VALIDATION.md) has the step that confirms it. *high* means documented
|
|
243
|
+
and used the same way by Pixelcade's own scripts; *medium* means documented only; *low* means our own
|
|
244
|
+
guess. Hardware results so far are from pixelweb 8.0.17 on a V2 board (2026-10-22).
|
|
245
|
+
|
|
246
|
+
| Assumption | Confidence | Status |
|
|
247
|
+
|---|---|---|
|
|
248
|
+
| `PX-GET-API` | high | hardware-verified |
|
|
249
|
+
| `PX-INFO` | medium | hardware-verified |
|
|
250
|
+
| `PX-CURRENTGAME` | medium | hardware-verified |
|
|
251
|
+
| `PX-LISTS` | low | hardware-contradicted |
|
|
252
|
+
| `PX-IMAGES-LIST` | high | hardware-verified |
|
|
253
|
+
| `PX-REPLIES` | low | hardware-verified |
|
|
254
|
+
| `PX-TEXT` | medium | hardware-verified |
|
|
255
|
+
| `PX-QUEUE` | medium | hardware-verified |
|
|
256
|
+
| `PX-HOLD` | medium | hardware-verified |
|
|
257
|
+
| `PX-FRONTEND` | high | hardware-verified |
|
|
258
|
+
| `PX-WIDGETS` | medium | hardware-verified |
|
|
259
|
+
| `PX-ATTRACT-LOCK` | medium | hardware-verified |
|
|
260
|
+
| `PX-DEFAULT-MARQUEE` | low | hardware-verified |
|
|
261
|
+
| `PX-ZONES` | medium | simulator-only |
|
|
262
|
+
| `PX-BRIGHTNESS` | medium | simulator-only |
|
|
263
|
+
| `PX-POWER` | high | simulator-only |
|
|
264
|
+
|
|
265
|
+
## Not yet included
|
|
266
|
+
|
|
267
|
+
- Pulse **batch zone control**, **MAX7219** 7-segment/dot-matrix displays, and **mini LCD** (`/ministats`)
|
|
268
|
+
endpoints, which Pixelcade documents but this server doesn't wrap yet.
|
|
269
|
+
- Editing the attract-mode playlist, and OAuth-backed widgets (Spotify, Plex, YouTube, Google Calendar).
|
|
270
|
+
- Uploading custom artwork (`/upload`): useful, but it writes files on the arcade, so it should be opt-in.
|
|
271
|
+
|
|
272
|
+
## Development
|
|
273
|
+
|
|
274
|
+
```sh
|
|
275
|
+
pip install -e ".[dev]"
|
|
276
|
+
pytest # validators, HTTP client, tools via an in-process MCP client, dry run, faults, CLI, stdio, HTTP + Access
|
|
277
|
+
ruff check . && ruff format --check .
|
|
278
|
+
mypy # strict
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
Tests run against the wire-level fake pixelweb (`sim/fake_pixelweb.py`), which records each request's raw
|
|
282
|
+
path, so the exact percent-encoding on the wire is asserted. To poke at the tools interactively:
|
|
283
|
+
`npx @modelcontextprotocol/inspector mcp-server-pixelcade`.
|
|
284
|
+
|
|
285
|
+
## License
|
|
286
|
+
|
|
287
|
+
MIT. See [LICENSE](https://github.com/SDNick484/mcp-server-pixelcade/blob/main/LICENSE).
|