mcp-server-batocera 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_batocera-0.1.0/LICENSE +21 -0
- mcp_server_batocera-0.1.0/PKG-INFO +422 -0
- mcp_server_batocera-0.1.0/README.md +388 -0
- mcp_server_batocera-0.1.0/pyproject.toml +64 -0
- mcp_server_batocera-0.1.0/setup.cfg +4 -0
- mcp_server_batocera-0.1.0/src/batocera_mcp/__init__.py +5 -0
- mcp_server_batocera-0.1.0/src/batocera_mcp/assumptions.py +299 -0
- mcp_server_batocera-0.1.0/src/batocera_mcp/cli.py +176 -0
- mcp_server_batocera-0.1.0/src/batocera_mcp/client.py +325 -0
- mcp_server_batocera-0.1.0/src/batocera_mcp/config.py +158 -0
- mcp_server_batocera-0.1.0/src/batocera_mcp/doctor.py +190 -0
- mcp_server_batocera-0.1.0/src/batocera_mcp/logsafe.py +309 -0
- mcp_server_batocera-0.1.0/src/batocera_mcp/lookup.py +60 -0
- mcp_server_batocera-0.1.0/src/batocera_mcp/probe.py +133 -0
- mcp_server_batocera-0.1.0/src/batocera_mcp/ratelimit.py +226 -0
- mcp_server_batocera-0.1.0/src/batocera_mcp/registry.py +141 -0
- mcp_server_batocera-0.1.0/src/batocera_mcp/remote.py +516 -0
- mcp_server_batocera-0.1.0/src/batocera_mcp/retroarch.py +143 -0
- mcp_server_batocera-0.1.0/src/batocera_mcp/server.py +796 -0
- mcp_server_batocera-0.1.0/src/batocera_mcp/sim/__init__.py +1 -0
- mcp_server_batocera-0.1.0/src/batocera_mcp/sim/fake_cabinet.py +425 -0
- mcp_server_batocera-0.1.0/src/batocera_mcp/tasks.py +174 -0
- mcp_server_batocera-0.1.0/src/batocera_mcp/tools.py +29 -0
- mcp_server_batocera-0.1.0/src/mcp_server_batocera.egg-info/PKG-INFO +422 -0
- mcp_server_batocera-0.1.0/src/mcp_server_batocera.egg-info/SOURCES.txt +42 -0
- mcp_server_batocera-0.1.0/src/mcp_server_batocera.egg-info/dependency_links.txt +1 -0
- mcp_server_batocera-0.1.0/src/mcp_server_batocera.egg-info/entry_points.txt +2 -0
- mcp_server_batocera-0.1.0/src/mcp_server_batocera.egg-info/requires.txt +11 -0
- mcp_server_batocera-0.1.0/src/mcp_server_batocera.egg-info/top_level.txt +1 -0
- mcp_server_batocera-0.1.0/tests/test_assumptions.py +86 -0
- mcp_server_batocera-0.1.0/tests/test_cli.py +148 -0
- mcp_server_batocera-0.1.0/tests/test_http_tools.py +124 -0
- mcp_server_batocera-0.1.0/tests/test_logsafe.py +521 -0
- mcp_server_batocera-0.1.0/tests/test_lookup.py +41 -0
- mcp_server_batocera-0.1.0/tests/test_playtracker.py +561 -0
- mcp_server_batocera-0.1.0/tests/test_probe.py +68 -0
- mcp_server_batocera-0.1.0/tests/test_ratelimit.py +382 -0
- mcp_server_batocera-0.1.0/tests/test_registry.py +175 -0
- mcp_server_batocera-0.1.0/tests/test_remote.py +571 -0
- mcp_server_batocera-0.1.0/tests/test_stdio.py +25 -0
- mcp_server_batocera-0.1.0/tests/test_tasks.py +425 -0
- mcp_server_batocera-0.1.0/tests/test_tool_annotations.py +18 -0
- mcp_server_batocera-0.1.0/tests/test_tools.py +865 -0
- mcp_server_batocera-0.1.0/tests/test_units.py +200 -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,422 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mcp-server-batocera
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MCP server for Batocera retro-gaming cabinets, via the EmulationStation web API and RetroArch network commands
|
|
5
|
+
Author: Nicholas E. Andrade
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/SDNick484/mcp-server-batocera
|
|
8
|
+
Project-URL: Issues, https://github.com/SDNick484/mcp-server-batocera/issues
|
|
9
|
+
Project-URL: Changelog, https://github.com/SDNick484/mcp-server-batocera/blob/main/CHANGELOG.md
|
|
10
|
+
Keywords: mcp,mcp-server,batocera,emulationstation,retroarch,arcade,home-assistant
|
|
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-batocera
|
|
36
|
+
|
|
37
|
+
An [MCP](https://modelcontextprotocol.io) server for a **[Batocera](https://batocera.org)** retro-gaming
|
|
38
|
+
cabinet. It lets an MCP client such as Claude browse the game library, see what's playing, launch and
|
|
39
|
+
quit games, put messages on screen, and, while a RetroArch game runs, pause, save or load state, take
|
|
40
|
+
screenshots and reset.
|
|
41
|
+
|
|
42
|
+
The repo also includes **playtracker**, a small hook script for the cabinet that publishes what's being
|
|
43
|
+
played to MQTT with Home Assistant discovery, plus what the cabinet is doing (screensaver, menu, playing,
|
|
44
|
+
off) for automations. It doesn't log screensaver activity.
|
|
45
|
+
|
|
46
|
+
> **Status: early, partly tested on hardware.** Built from Batocera's EmulationStation and RetroArch sources
|
|
47
|
+
> and tested against a wire-level simulator; playtracker is also tested against a real Mosquitto broker.
|
|
48
|
+
> On a real cabinet (Batocera 43), every MCP tool has been checked: `doctor`, the library, launching and
|
|
49
|
+
> quitting, messages, RetroArch and dry run, and playtracker with Home Assistant (sessions, the short-session
|
|
50
|
+
> filter, broker outages, and ignoring the screensaver). playtracker's cabinet events (Screensaver, Activity,
|
|
51
|
+
> session start, Cabinet) aren't checked on hardware yet. See [HARDWARE_VALIDATION.md](https://github.com/SDNick484/mcp-server-batocera/blob/main/HARDWARE_VALIDATION.md) and the
|
|
52
|
+
> [verification table](#verification-status).
|
|
53
|
+
|
|
54
|
+
This is not an official Batocera project.
|
|
55
|
+
|
|
56
|
+
## How it works
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
MCP client ──stdio──▶ mcp-server-batocera ──HTTP :1234──▶ EmulationStation web API ─┐
|
|
60
|
+
└──UDP :55355──▶ RetroArch (while a game runs) ├─ Batocera cabinet
|
|
61
|
+
Home Assistant ◀──MQTT :1883── playtracker.py ◀── EmulationStation event scripts ──────┘
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
- **EmulationStation web API** (TCP 1234) runs inside the frontend, so it keeps answering during a game.
|
|
65
|
+
It covers the game library, the running game, launching, quitting and on-screen notifications.
|
|
66
|
+
- **RetroArch network commands** (UDP 55355) work only while a libretro core is running. They cover
|
|
67
|
+
pause, save/load state, screenshots, reset and in-game messages.
|
|
68
|
+
- **playtracker** runs on the cabinet on every EmulationStation event and publishes to Home Assistant's
|
|
69
|
+
Mosquitto broker. Nothing extra has to run on Batocera; it uses only Python's standard library.
|
|
70
|
+
|
|
71
|
+
## Remote access: stdio, or HTTP behind Cloudflare Access
|
|
72
|
+
|
|
73
|
+
There are two ways to run it:
|
|
74
|
+
|
|
75
|
+
- **stdio**: Claude Code or Claude Desktop launches it on a machine on the cabinet's LAN.
|
|
76
|
+
- **`serve --http`**: Streamable HTTP on `127.0.0.1:8717/batocera/mcp`, as a long-lived service (see
|
|
77
|
+
[Service and Cloudflare](#service-and-cloudflare)). Published through a Cloudflare Tunnel with Cloudflare
|
|
78
|
+
Access in front, it's one URL you add as a connector in Claude Code, Claude Desktop and the mobile app.
|
|
79
|
+
|
|
80
|
+
The HTTP transport is `remote.py`, shared byte for byte with the AV siblings (Onkyo, Shield TV, Harmony,
|
|
81
|
+
Sofabaton, webOS) through [mcp-server-shared](https://github.com/SDNick484/mcp-server-shared). The cabinet's
|
|
82
|
+
own APIs have **no authentication**: EmulationStation's web API and RetroArch's network commands trust
|
|
83
|
+
anything that can reach them. So the server is the gate, and it doesn't rely on Cloudflare's edge alone:
|
|
84
|
+
|
|
85
|
+
- **With Access configured, every request needs a valid Access assertion**, checked here: signature
|
|
86
|
+
(Access's published keys), the application's audience tag, issuer, expiry, and `MCP_ALLOWED_EMAILS` if
|
|
87
|
+
set. Anything else gets a plain 403, so a request that reaches the port from the LAN, around the tunnel,
|
|
88
|
+
is refused too.
|
|
89
|
+
- **Without Access, it serves only the machine it runs on.** It won't listen on anything but loopback,
|
|
90
|
+
won't start with a `--public-host`, and answers 403 to anything a tunnel or proxy forwarded.
|
|
91
|
+
`--insecure-no-auth` turns that off for a short trusted-LAN test; don't run the service with it.
|
|
92
|
+
- **Never expose ports 1234 or 55355 themselves.** Only this server's port goes through the tunnel.
|
|
93
|
+
|
|
94
|
+
## Cabinet setup
|
|
95
|
+
|
|
96
|
+
1. **Turn on public web access.** By default the API only answers the cabinet itself. Go to *Main Menu >
|
|
97
|
+
System Settings > Developer > Enable public web access*, then reboot.
|
|
98
|
+
2. **Optional, for the RetroArch tools:** SSH in (`root` / `linux`) and add this line to
|
|
99
|
+
`/userdata/system/batocera.conf`. It takes effect on the next game launch.
|
|
100
|
+
```
|
|
101
|
+
global.retroarch.network_cmd_enable=true
|
|
102
|
+
```
|
|
103
|
+
3. Give the cabinet a fixed IP (a DHCP reservation) and use the IP rather than `batocera.local`.
|
|
104
|
+
|
|
105
|
+
Neither the API nor RetroArch's port has authentication; anything on the LAN can already use them. Keep
|
|
106
|
+
them off the internet.
|
|
107
|
+
|
|
108
|
+
## Install the MCP server
|
|
109
|
+
|
|
110
|
+
```sh
|
|
111
|
+
python -m venv ~/.venvs/batocera && . ~/.venvs/batocera/bin/activate
|
|
112
|
+
pip install mcp-server-batocera
|
|
113
|
+
BATOCERA_URL=192.168.1.20 mcp-server-batocera doctor
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
From source instead (for the newest code, or to run the tests):
|
|
117
|
+
|
|
118
|
+
```sh
|
|
119
|
+
git clone https://github.com/SDNick484/mcp-server-batocera.git && cd mcp-server-batocera
|
|
120
|
+
python -m venv .venv && . .venv/bin/activate
|
|
121
|
+
pip install -e ".[dev]" && pytest
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
`doctor` checks each layer between this machine and the cabinet (TCP, the API, the library, the
|
|
125
|
+
running game, RetroArch), and when one fails, says what it means and which
|
|
126
|
+
[HARDWARE_VALIDATION.md](https://github.com/SDNick484/mcp-server-batocera/blob/main/HARDWARE_VALIDATION.md) step covers it. It changes nothing. It ends with the
|
|
127
|
+
[assumptions](#verification-status) that hardware contradicted (listed; the code no longer relies on them) and a
|
|
128
|
+
count of those not yet confirmed; `doctor --verbose` lists those too, and `doctor --json` always includes
|
|
129
|
+
both lists.
|
|
130
|
+
|
|
131
|
+
```json
|
|
132
|
+
{ "mcpServers": { "batocera": {
|
|
133
|
+
"command": "/path/to/mcp-server-batocera/.venv/bin/mcp-server-batocera",
|
|
134
|
+
"env": { "BATOCERA_URL": "192.168.1.20" } } } }
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
For Claude Code: `claude mcp add batocera -e BATOCERA_URL=192.168.1.20 -- /path/to/.venv/bin/mcp-server-batocera`.
|
|
138
|
+
|
|
139
|
+
| Setting | Default | What it does |
|
|
140
|
+
| ------------------------- | -------------------------------------- | ---------------------------------------------- |
|
|
141
|
+
| `BATOCERA_URL` | `http://batocera.local:1234` | EmulationStation's API; a bare host gets `:1234` |
|
|
142
|
+
| `BATOCERA_RETROARCH_PORT` | `55355` | RetroArch's network command port |
|
|
143
|
+
| `BATOCERA_TIMEOUT` | `10` | Seconds to wait for the API (1-60) |
|
|
144
|
+
| `BATOCERA_CONFIG_DIR` | `$XDG_CONFIG_HOME/mcp-server-batocera` | Where an optional `config.json` lives (`url`, `retroarch_port`, `timeout_s`) |
|
|
145
|
+
| `BATOCERA_NAME` | the cabinet's address | How results name the cabinet |
|
|
146
|
+
| `BATOCERA_DRY_RUN` | off | Read the cabinet, change nothing (`serve --dry-run`) |
|
|
147
|
+
| `BATOCERA_LOG_UNREDACTED` | off | Show LAN addresses in logs (`--no-redact`); secrets stay hidden |
|
|
148
|
+
| `BATOCERA_DEBUG` | off | Debug logging (`--debug`) |
|
|
149
|
+
|
|
150
|
+
`config.json` takes `url`, `retroarch_port`, `timeout_s`, `name` and `dry_run`; environment variables win.
|
|
151
|
+
|
|
152
|
+
### Trying it without the cabinet
|
|
153
|
+
|
|
154
|
+
```sh
|
|
155
|
+
mcp-server-batocera simulate --write-config /tmp/sim # a fake cabinet: EmulationStation over HTTP, RetroArch over UDP
|
|
156
|
+
export BATOCERA_CONFIG_DIR=/tmp/sim # in another terminal
|
|
157
|
+
mcp-server-batocera doctor
|
|
158
|
+
mcp-server-batocera call find_games query=mario # one tool call, exactly as the model makes it
|
|
159
|
+
mcp-server-batocera call launch_game system=snes game_id=<id from the result above>
|
|
160
|
+
mcp-server-batocera call --dry-run quit_game # reports what it would send
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
The simulator has a small library across a RetroArch system and a standalone one (GameCube), so both
|
|
164
|
+
paths are there to try. The tests use the same simulator.
|
|
165
|
+
|
|
166
|
+
### On Alpine (e.g. a Proxmox LXC)
|
|
167
|
+
|
|
168
|
+
`deploy/alpine/` installs it on an always-on Alpine machine on the cabinet's LAN, in the sibling
|
|
169
|
+
servers' `deploy/alpine/` layout so they can share an LXC: a venv in `/opt/mcp-server-batocera`, a
|
|
170
|
+
`mcp-batocera` system user, every setting in `/etc/mcp-server-batocera/env`, an OpenRC service
|
|
171
|
+
(`/etc/init.d/mcp-server-batocera`), and `mcp-server-batocera` on `PATH`, which runs as that user with
|
|
172
|
+
those settings, for `doctor` and `call` from a shell.
|
|
173
|
+
|
|
174
|
+
```sh
|
|
175
|
+
git clone https://github.com/SDNick484/mcp-server-batocera.git && cd mcp-server-batocera
|
|
176
|
+
sh deploy/alpine/install.sh # as root; re-run to upgrade
|
|
177
|
+
vim /etc/mcp-server-batocera/env # BATOCERA_URL=<the cabinet's IP>
|
|
178
|
+
mcp-server-batocera doctor
|
|
179
|
+
mcp-server-batocera call find_games query=mario
|
|
180
|
+
rc-update add mcp-server-batocera default && rc-service mcp-server-batocera start
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
A variable set in your shell beats the env file, so one-off overrides work as usual
|
|
184
|
+
(`BATOCERA_DRY_RUN=1 mcp-server-batocera call ...`).
|
|
185
|
+
|
|
186
|
+
**Both arcade servers at once.** `deploy/alpine/setup-arcade.sh` installs this server and
|
|
187
|
+
[mcp-server-pixelcade](https://github.com/SDNick484/mcp-server-pixelcade) from GitHub into
|
|
188
|
+
`/srv/src/`, points both at the cabinet, runs both doctors, then enables both services at boot and
|
|
189
|
+
restarts them (so a re-run's new code is the code that runs). Run it as root on the Alpine machine, or
|
|
190
|
+
from elsewhere through the Proxmox host:
|
|
191
|
+
|
|
192
|
+
```sh
|
|
193
|
+
ssh root@<proxmox host> "pct exec <container id> -- env ARCADE_HOST=<cabinet IP> sh -s" < deploy/alpine/setup-arcade.sh
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
`ARCADE_REF` picks a branch, tag or commit (default `main`), and `ARCADE_REF_BATOCERA` or
|
|
197
|
+
`ARCADE_REF_PIXELCADE` one server's ref instead; re-running updates the code and keeps the settings. CI runs `deploy/alpine/smoke-test.sh` in a stock `python:3.12-alpine` container: install,
|
|
198
|
+
upgrade, the test suite on musl, `doctor` and `call` through the command on `PATH` against the
|
|
199
|
+
simulator, the OpenRC service (as its user, an MCP call over HTTP, Access settings from the env file),
|
|
200
|
+
and `setup-arcade.sh` itself.
|
|
201
|
+
|
|
202
|
+
### Service and Cloudflare
|
|
203
|
+
|
|
204
|
+
The service runs `/opt/mcp-server-batocera/run.sh serve --http` under `supervise-daemon` (restarted if it
|
|
205
|
+
exits) as `mcp-batocera`, logging to `/var/log/mcp-server-batocera/server.log`. Its settings are the
|
|
206
|
+
`MCP_HTTP_*`, `MCP_PUBLIC_HOSTS` and `CF_ACCESS_*` lines in `/etc/mcp-server-batocera/env`:
|
|
207
|
+
|
|
208
|
+
| Flag | Variable | Default |
|
|
209
|
+
| --- | --- | --- |
|
|
210
|
+
| `--bind` | `MCP_HTTP_BIND` | `127.0.0.1` |
|
|
211
|
+
| `--port` | `MCP_HTTP_PORT` | `8717` (the siblings use 8711 to 8716, Pixelcade 8718) |
|
|
212
|
+
| `--path` | `MCP_HTTP_PATH` | `/batocera/mcp` |
|
|
213
|
+
| `--public-host` | `MCP_PUBLIC_HOSTS` | none: only loopback Host headers pass |
|
|
214
|
+
| | `CF_ACCESS_TEAM_DOMAIN`, `CF_ACCESS_AUD` | none: serves only this machine |
|
|
215
|
+
| | `MCP_ALLOWED_EMAILS` | anyone Access lets in |
|
|
216
|
+
|
|
217
|
+
To use it as a connector, with cloudflared running on the same machine (keep the `127.0.0.1` bind):
|
|
218
|
+
|
|
219
|
+
1. **Tunnel route:** a public hostname, say `mcp.example.com`, path `batocera/*`, to
|
|
220
|
+
`http://localhost:8717`. Several servers can share the hostname, one path each.
|
|
221
|
+
2. **Access application** (self-hosted) covering that hostname and path, with a policy for who may use it
|
|
222
|
+
and Managed OAuth turned on, so Claude's connector can sign in through Access. Note its AUD tag.
|
|
223
|
+
3. **The env file:** `MCP_PUBLIC_HOSTS=mcp.example.com`, `CF_ACCESS_TEAM_DOMAIN=<team>.cloudflareaccess.com`,
|
|
224
|
+
`CF_ACCESS_AUD=<the AUD tag>`, then `rc-service mcp-server-batocera restart`.
|
|
225
|
+
4. **The connector:** `https://mcp.example.com/batocera/mcp`.
|
|
226
|
+
|
|
227
|
+
`GET /healthz` answers without auth, for monitoring. Details are in `src/batocera_mcp/remote.py`.
|
|
228
|
+
|
|
229
|
+
## Tools
|
|
230
|
+
|
|
231
|
+
All tools are prefixed with `batocera_` to eliminate namespace collisions when multiple MCP servers run concurrently in an agent's context. CLI commands support shorthand (e.g., `mcp-server-batocera call find_games query=mario`).
|
|
232
|
+
|
|
233
|
+
| Tool | What it does |
|
|
234
|
+
| ---------------------------- | --------------------------------------------------------------------------------------------- |
|
|
235
|
+
| `batocera_get_status` | Reachable, EmulationStation version, the running game, busy (scraping/updating), RetroArch state |
|
|
236
|
+
| `batocera_list_systems` | Systems with games (not collections or the picture viewer): counts, played, favorites, most played |
|
|
237
|
+
| `batocera_find_games` | Search by name (punctuation-insensitive), filter by genre / players, list favorites / most played / most time / recent / random; pages with `offset` |
|
|
238
|
+
| `batocera_get_game` | Description, developer, publisher, rating, players, play count and time |
|
|
239
|
+
| `batocera_launch_game` | Start a game by system + id; refuses while another game runs; confirms it started |
|
|
240
|
+
| `batocera_quit_game` | Quit the running game (**destructive**: unsaved progress is lost) |
|
|
241
|
+
| `batocera_show_message` | Over the game (RetroArch), or a frontend notification or popup |
|
|
242
|
+
| `batocera_reload_games` | Rescan game lists after adding ROMs (rate-limited) |
|
|
243
|
+
| `batocera_pause_game` | Pause or resume (RetroArch) |
|
|
244
|
+
| `batocera_save_state` | Save to the current slot (RetroArch, rate-limited) |
|
|
245
|
+
| `batocera_load_state` | Load from the current slot (RetroArch, **destructive**, rate-limited) |
|
|
246
|
+
| `batocera_take_screenshot` | Save a screenshot on the cabinet (RetroArch) |
|
|
247
|
+
| `batocera_reset_game` | Reset the game (RetroArch, **destructive**, rate-limited) |
|
|
248
|
+
| `batocera_adjust_volume` | Volume up or down one step (0.5 dB), or toggle mute (RetroArch) |
|
|
249
|
+
| `batocera_change_save_slot` | Next or previous save-state slot, used by `save_state` / `load_state` (RetroArch) |
|
|
250
|
+
|
|
251
|
+
Play counts and play time come from EmulationStation itself (it records them in its game lists), so
|
|
252
|
+
"what have we played most?" works without Home Assistant.
|
|
253
|
+
|
|
254
|
+
Every tool that changes something returns the same structured result as the sibling servers:
|
|
255
|
+
|
|
256
|
+
```json
|
|
257
|
+
{ "cabinet": "Game Room", "outcome": "done", "detail": "Launched Galaga (mame).",
|
|
258
|
+
"sent": ["POST /launch /userdata/roms/mame/galaga.zip"], "warnings": [] }
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
`outcome` is `done`, `unchanged` (already the case, so nothing was sent) or `dry_run`; `sent` is what
|
|
262
|
+
went to the cabinet, or in a dry run what would have. Failures are MCP error results with a message
|
|
263
|
+
saying what to fix.
|
|
264
|
+
|
|
265
|
+
## Safety design
|
|
266
|
+
|
|
267
|
+
- **Games are an allow-list.** `launch_game` takes a system and EmulationStation's game id. The server
|
|
268
|
+
looks the id up in EmulationStation's own list and sends that entry's path; the model never supplies a
|
|
269
|
+
path.
|
|
270
|
+
- **Endpoints that change the system aren't reachable:** `/quit`, `/restart`, `/addgames`,
|
|
271
|
+
`/removegames`, metadata and media uploads, `/input` (simulated key presses) and `/storage/event`.
|
|
272
|
+
- **One game at a time, on purpose.** `launch_game` won't replace a running game; quitting is a separate
|
|
273
|
+
tool marked destructive, so MCP clients ask before ending someone's game.
|
|
274
|
+
- **Rate limits against runaway loops.** `launch_game` and `quit_game` share a budget of 3 (one more every
|
|
275
|
+
5 s), and `reload_games` has its own of 2 (one more every 20 s). Past that, a call is refused with how long
|
|
276
|
+
to wait. Dry runs and calls that change nothing (already running, nothing to quit) don't count.
|
|
277
|
+
- **RetroArch gets constants only** (`PAUSE_TOGGLE`, `SAVE_STATE`, `LOAD_STATE`, `SCREENSHOT`, `RESET`),
|
|
278
|
+
plus `SHOW_MSG` with validated single-line text.
|
|
279
|
+
- Every tool has a title and MCP annotations. Errors say what to do ("turn on public web access",
|
|
280
|
+
"add `global.retroarch.network_cmd_enable=true`"), and logs go to stderr.
|
|
281
|
+
- **Over HTTP, the server is the gate** (the cabinet's APIs have none): Access assertions are checked on
|
|
282
|
+
every request, and without Access it serves only its own machine. See
|
|
283
|
+
[Remote access](#remote-access-stdio-or-http-behind-cloudflare-access).
|
|
284
|
+
|
|
285
|
+
## Play tracking in Home Assistant (playtracker)
|
|
286
|
+
|
|
287
|
+
### What gets logged, and what doesn't
|
|
288
|
+
|
|
289
|
+
Batocera's screensaver (dim, black, random video, slideshow, suspend) **never starts a game**. The
|
|
290
|
+
random-video and slideshow screensavers *select* games to show their videos, which fires EmulationStation's
|
|
291
|
+
`game-selected` event. (A Pixelcade marquee doesn't follow those: Pixelcade's own screensaver hook locks
|
|
292
|
+
it in attract mode until the screensaver ends.) playtracker only logs `game-start`/`game-end`, which fire
|
|
293
|
+
only when a game actually launches.
|
|
294
|
+
|
|
295
|
+
Two filters on top of that:
|
|
296
|
+
- **Screensaver launches.** With *Screensaver controls* on, pressing START on a screensaver video
|
|
297
|
+
launches that game. playtracker recognizes this case: the launched game is the one the screensaver was
|
|
298
|
+
showing, and the screensaver was active within the last 10 seconds. With `IGNORE_SCREENSAVER_LAUNCHES=yes`
|
|
299
|
+
(the default) it logs nothing for that session.
|
|
300
|
+
- **Short sessions.** Sessions under `MIN_SESSION_SECONDS` (default 60) aren't logged.
|
|
301
|
+
|
|
302
|
+
### Install
|
|
303
|
+
|
|
304
|
+
1. In Home Assistant, create a user for the cabinet (*Settings > People > Users*, e.g. `batocera`). The
|
|
305
|
+
Mosquitto add-on accepts Home Assistant users.
|
|
306
|
+
2. Copy the files to the cabinet, from a clone of this repository (the PyPI package is the MCP server
|
|
307
|
+
only; playtracker runs on the cabinet and isn't part of it):
|
|
308
|
+
```sh
|
|
309
|
+
scp playtracker/playtracker.py root@192.168.1.20:/userdata/system/configs/emulationstation/scripts/
|
|
310
|
+
scp playtracker/playtracker.conf.example root@192.168.1.20:/userdata/system/configs/playtracker.conf
|
|
311
|
+
ssh root@192.168.1.20 'chmod +x /userdata/system/configs/emulationstation/scripts/playtracker.py;
|
|
312
|
+
chmod 600 /userdata/system/configs/playtracker.conf'
|
|
313
|
+
```
|
|
314
|
+
3. Edit `playtracker.conf` (broker host, user, password, device name), then restart EmulationStation or
|
|
315
|
+
reboot. A device named after `DEVICE_NAME` (*Batocera* unless you change it) appears in Home
|
|
316
|
+
Assistant under MQTT.
|
|
317
|
+
|
|
318
|
+
Scripts directly in `.../emulationstation/scripts/` get every event, with the event name first. This
|
|
319
|
+
doesn't interfere with Pixelcade's per-event scripts in the subfolders. To debug, read
|
|
320
|
+
`/tmp/playtracker/playtracker.log` on the cabinet.
|
|
321
|
+
|
|
322
|
+
### Entities
|
|
323
|
+
|
|
324
|
+
Home Assistant names the entities after the device, so with the default `DEVICE_NAME=Batocera` they
|
|
325
|
+
come out as below. Rename the device and they follow: `DEVICE_NAME=Game Room Arcade` gives
|
|
326
|
+
`sensor.game_room_arcade_now_playing`, and so on. Check the device page for the exact IDs.
|
|
327
|
+
|
|
328
|
+
| Entity | State | Attributes |
|
|
329
|
+
| --------------------------------------- | ---------------------- | ---------------------------------------------------- |
|
|
330
|
+
| `sensor.batocera_now_playing` | game name, or `Idle` | system, rom, started, source |
|
|
331
|
+
| `binary_sensor.batocera_playing` | on while a game runs | |
|
|
332
|
+
| `sensor.batocera_now_playing_system` | system name | |
|
|
333
|
+
| `sensor.batocera_last_session` | last game played | system, rom, started, ended, duration_s |
|
|
334
|
+
| `sensor.batocera_last_session_length` | minutes | |
|
|
335
|
+
| `event.batocera_game_session` | `session_start`, `session_end` | game, system, rom, source, started (+ ended, duration_s at the end) |
|
|
336
|
+
| `binary_sensor.batocera_screensaver` | on while the screensaver runs | type (dim, black, random video, slideshow, suspend) |
|
|
337
|
+
| `sensor.batocera_activity` | `menu`, `screensaver`, `playing`, `off` | |
|
|
338
|
+
| `event.batocera_cabinet` | `start`, `wake`, `sleep`, `quit`, `reboot`, `shutdown` | at |
|
|
339
|
+
| availability | `batocera/<id>/status` | online on start and wake, offline on quit, reboot, shutdown |
|
|
340
|
+
|
|
341
|
+
Each logged session is one `session_end` event, so it shows up in the logbook, history and automations.
|
|
342
|
+
`session_start` fires as a logged game starts (not for ignored screensaver launches).
|
|
343
|
+
|
|
344
|
+
**For automations** (lights when a game starts or the screensaver kicks in), use *Activity*. It describes
|
|
345
|
+
what's on screen rather than what's logged: a game launched from the screensaver reads `playing` even when
|
|
346
|
+
it isn't logged. It has no availability topic, so it shows `off` after a quit, reboot or shutdown instead of
|
|
347
|
+
*unavailable*. When START on a screensaver video launches a game, *Activity* can pass through `menu` for a
|
|
348
|
+
moment on its way to `playing` (EmulationStation's two events race), so give such triggers a `for:` of a
|
|
349
|
+
couple of seconds. EmulationStation's `sleep` and `wake` are its idle sleep during a dim or black screensaver,
|
|
350
|
+
not a system suspend, so the cabinet stays online through them, and a reboot or shutdown is one *Cabinet*
|
|
351
|
+
event, not a `quit` as well.
|
|
352
|
+
|
|
353
|
+
Daily play time:
|
|
354
|
+
|
|
355
|
+
```yaml
|
|
356
|
+
sensor:
|
|
357
|
+
- platform: history_stats
|
|
358
|
+
name: Arcade play time today
|
|
359
|
+
entity_id: binary_sensor.batocera_playing
|
|
360
|
+
state: "on"
|
|
361
|
+
type: time
|
|
362
|
+
start: "{{ today_at() }}"
|
|
363
|
+
end: "{{ now() }}"
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
If the broker is unreachable (Home Assistant restarting, say), messages are queued in `/tmp/playtracker`
|
|
367
|
+
and sent with the next event: the latest value of each entity, and logged sessions. Other events
|
|
368
|
+
(`session_start`, *Cabinet*) are dropped, because a late one would run its automation at the wrong time: a
|
|
369
|
+
shutdown event would arrive at the next boot. `/tmp` is a tmpfs, emptied by any restart. If the cabinet loses power, `status` stays
|
|
370
|
+
`online` until it next boots, because the script doesn't keep a connection open for an MQTT last will.
|
|
371
|
+
|
|
372
|
+
## Verification status
|
|
373
|
+
|
|
374
|
+
Every Batocera behavior the code relies on is listed in `src/batocera_mcp/assumptions.py`, with its
|
|
375
|
+
source and how sure we are; the code cites each id where it depends on it, and
|
|
376
|
+
[HARDWARE_VALIDATION.md](https://github.com/SDNick484/mcp-server-batocera/blob/main/HARDWARE_VALIDATION.md) has the step that confirms it. *high* means read
|
|
377
|
+
directly in the code that implements it; *medium* means read in source but dependent on settings,
|
|
378
|
+
timing or versions; *low* means our own guess. Hardware results are from Batocera 43 (2026-10-21 to 2026-10-24); every assumption except `B-PT-LIFECYCLE` and `B-RA-AUDIO-SLOT` is confirmed.
|
|
379
|
+
|
|
380
|
+
| Assumption | Confidence | Status |
|
|
381
|
+
|---|---|---|
|
|
382
|
+
| `B-ES-PUBLIC-ACCESS` | high | hardware-verified |
|
|
383
|
+
| `B-ES-DURING-GAME` | medium | hardware-verified |
|
|
384
|
+
| `B-ES-RUNNINGGAME` | high | hardware-verified |
|
|
385
|
+
| `B-ES-LAUNCH` | high | hardware-verified |
|
|
386
|
+
| `B-ES-EMUKILL` | medium | hardware-verified |
|
|
387
|
+
| `B-ES-GAME-ID` | high | hardware-verified |
|
|
388
|
+
| `B-ES-METADATA` | high | hardware-verified |
|
|
389
|
+
| `B-ES-SYSTEMS` | high | hardware-verified |
|
|
390
|
+
| `B-ES-THREADS` | high | hardware-verified |
|
|
391
|
+
| `B-ES-MESSAGES` | medium | hardware-verified |
|
|
392
|
+
| `B-RA-ENABLE` | medium | hardware-verified |
|
|
393
|
+
| `B-RA-STATUS` | medium | hardware-verified |
|
|
394
|
+
| `B-RA-COMMANDS` | medium | hardware-verified |
|
|
395
|
+
| `B-RA-AUDIO-SLOT` | medium | simulator-only |
|
|
396
|
+
| `B-PT-SCRIPTS` | high | hardware-verified |
|
|
397
|
+
| `B-PT-GAME-ARGS` | high | hardware-verified |
|
|
398
|
+
| `B-PT-SCREENSAVER` | high | hardware-verified |
|
|
399
|
+
| `B-PT-SCREENSAVER-START` | medium | hardware-verified |
|
|
400
|
+
| `B-PT-TMP` | medium | hardware-verified |
|
|
401
|
+
| `B-PT-LIFECYCLE` | high | simulator-only |
|
|
402
|
+
| `B-PT-HA-MQTT` | high | hardware-verified |
|
|
403
|
+
|
|
404
|
+
## Not yet included
|
|
405
|
+
|
|
406
|
+
- Fixed-command SSH tools (CPU temperature, disk space, restart EmulationStation). These need SSH
|
|
407
|
+
credentials, so they'd be opt-in.
|
|
408
|
+
- Adding ROMs (`/addgames`), or editing metadata and media.
|
|
409
|
+
- Choosing the save-state slot. RetroArch's network commands only step it up or down.
|
|
410
|
+
|
|
411
|
+
## Development
|
|
412
|
+
|
|
413
|
+
```sh
|
|
414
|
+
pip install -e ".[dev]"
|
|
415
|
+
pytest # tools via an in-process MCP client against the simulator, faults, CLI, playtracker, stdio, HTTP + Access
|
|
416
|
+
ruff check . && ruff format --check .
|
|
417
|
+
mypy # strict, src/ and playtracker/
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
## License
|
|
421
|
+
|
|
422
|
+
MIT. See [LICENSE](https://github.com/SDNick484/mcp-server-batocera/blob/main/LICENSE).
|